Skip to content

Commit

Permalink
Merge pull request #29 from tolbertam/16-custom-reporters
Browse files Browse the repository at this point in the history
ScheduledReporter with console, csv and graphite implementations.
  • Loading branch information
mikejihbe committed Mar 15, 2016
2 parents f70de14 + d120d1e commit 375e78d
Show file tree
Hide file tree
Showing 14 changed files with 646 additions and 2 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ metricsServer.addMetric('com.co.thingD', meter);
metricsServer.addMetric('com.co.thingE', timer);
```

**Setting up a Reporter**

A reporting interface exists for reporting metrics on a recurring interval. Reporters can be found in [reporting/](reporting).

```javascript
// Report to console every 1000ms.
var report = new metrics.Report();
report.addMetric('com.co.thingA', counter);
var reporter = new metrics.ConsoleReporter(report);

reporter.start(1000);
```

Advanced Usage
--------------
Expand Down
7 changes: 6 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,10 @@ exports.Timer = Metrics.Timer;
exports.Server = Reporting.Server;
exports.Report = Reporting.Report;

exports.version = '0.1.5';
exports.ScheduledReporter = Reporting.ScheduledReporter;
exports.ConsoleReporter = Reporting.ConsoleReporter;
exports.CsvReporter = Reporting.CsvReporter;
exports.GraphiteReporter = Reporting.GraphiteReporter;

exports.version = '0.1.10';

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "metrics",
"description": "A node.js port of Coda Hale's metrics library. In use at Yammer.",
"version": "0.1.9",
"version": "0.1.10",
"repository": {
"type": "git",
"url": "git://github.com/mikejihbe/metrics.git"
Expand Down
98 changes: 98 additions & 0 deletions reporting/console-reporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use strict';
var ScheduledReporter = require('./scheduled-reporter.js'),
util = require('util');

/**
* A custom reporter that prints all metrics on console.log at a defined interval.
* @param {Report} registry report instance whose metrics to report on.
* @constructor
*/
function ConsoleReporter(registry) {
ConsoleReporter.super_.call(this, registry);
}

util.inherits(ConsoleReporter, ScheduledReporter);

ConsoleReporter.prototype.report = function() {
var metrics = this.getMetrics();

if(metrics.counters.length != 0) {
printWithBanner('Counters');
metrics.counters.forEach(function (counter) {
printCounter(counter);
});
console.log();
}

if(metrics.meters.length != 0) {
printWithBanner('Meters');
metrics.meters.forEach(function (meter) {
printMeter(meter);
});
console.log();
}

if(metrics.timers.length != 0) {
printWithBanner('Timers');
metrics.timers.forEach(function (timer) {
// Don't log timer if its recorded no metrics.
if(timer.min() != null) {
printTimer(timer);
}
});
console.log();
}
};

function printWithBanner(name) {
var dashLength = 80 - name.length - 1;
var dashes = "";
for(var i = 0; i < dashLength; i++) {
dashes += '-';
}
console.log('%s %s', name, dashes);
}

function ff(value) {
var fixed = value.toFixed(2);
return fixed >= 10 || fixed < 0 ? fixed : " " + fixed;
}

function printCounter(counter) {
console.log(counter.name);
console.log(' count = %d', counter.count);
}

function printMeter(meter) {
console.log(meter.name);
console.log(' count = %d', meter.count);
console.log(' mean rate = %s events/%s', ff(meter.meanRate()), 'second');
console.log(' 1-minute rate = %s events/%s', ff(meter.oneMinuteRate()), 'second');
console.log(' 5-minute rate = %s events/%s', ff(meter.fiveMinuteRate()), 'second');
console.log(' 15-minute rate = %s events/%s', ff(meter.fifteenMinuteRate()), 'second');
}

function printTimer(timer) {
console.log(timer.name);
console.log(' count = %d', timer.count());
console.log(' mean rate = %s events/%s', ff(timer.meanRate()), 'second');
console.log(' 1-minute rate = %s events/%s', ff(timer.oneMinuteRate()), 'second');
console.log(' 5-minute rate = %s events/%s', ff(timer.fiveMinuteRate()), 'second');
console.log(' 15-minute rate = %s events/%s', ff(timer.fifteenMinuteRate()), 'second');

var percentiles = timer.percentiles([.50,.75,.95,.98,.99,.999]);

console.log(' min = %s %s', ff(timer.min()), 'milliseconds');
console.log(' max = %s %s', ff(timer.max()), 'milliseconds');
console.log(' mean = %s %s', ff(timer.mean()), 'milliseconds');
console.log(' stddev = %s %s', ff(timer.stdDev()), 'milliseconds');
console.log(' 50%% <= %s %s', ff(percentiles[.50]), 'milliseconds');
console.log(' 75%% <= %s %s', ff(percentiles[.75]), 'milliseconds');
console.log(' 95%% <= %s %s', ff(percentiles[.95]), 'milliseconds');
console.log(' 98%% <= %s %s', ff(percentiles[.98]), 'milliseconds');
console.log(' 99%% <= %s %s', ff(percentiles[.99]), 'milliseconds');
console.log(' 99.9%% <= %s %s', ff(percentiles[.999]), 'milliseconds');
}

module.exports = ConsoleReporter;

108 changes: 108 additions & 0 deletions reporting/csv-reporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
'use strict';
var ScheduledReporter = require('./scheduled-reporter.js'),
util = require('util'),
fs = require('fs');

/**
* A custom reporter that will create a csv file for each metric that is appended to on the defined reporting interval.
* @param {Report} registry report instance whose metrics to report on.
* @param {String} directory Directory to put csv files in.
* @constructor
*/
function CsvReporter(registry, directory) {
CsvReporter.super_.call(this, registry);
this.directory = directory;
}

util.inherits(CsvReporter, ScheduledReporter);

CsvReporter.prototype.report = function() {
var metrics = this.getMetrics();
var self = this;

var timestamp = (new Date).getTime() / 1000;

metrics.counters.forEach(function(counter) {
self.reportCounter.bind(self)(counter, timestamp);
});

metrics.meters.forEach(function(meter) {
self.reportMeter.bind(self)(meter, timestamp);
});

metrics.timers.forEach(function(timer) {
// Don't log timer if its recorded no metrics.
if(timer.min() != null) {
self.reportTimer.bind(self)(timer, timestamp);
}
})
};

CsvReporter.prototype.write = function(timestamp, name, header, line, values) {
var file = util.format('%s/%s.csv', this.directory, name);
// copy params and add line to the beginning and pass as arguments to util.format
// this operates like a quasi 'vsprintf'.
var params = values.slice();
params.unshift(line + "\n");
var data = util.format.apply(util, params);
data = util.format('%d,%s', timestamp, data);
fs.exists(file, function(exists) {
if(!exists) {
// include header if file didn't previously exist
data = util.format("t,%s\n%s", header, data);
}
fs.appendFile(file, data);
});
};

CsvReporter.prototype.reportCounter = function(counter, timestamp) {
var write = this.write.bind(this);

write(timestamp, counter.name, 'count', '%d', [counter.count]);
};

CsvReporter.prototype.reportMeter = function(meter, timestamp) {
var write = this.write.bind(this);

write(timestamp, meter.name,
'count,mean_rate,m1_rate,m5_rate,m15_rate,rate_unit',
'%d,%d,%d,%d,%d,events/%s', [
meter.count,
meter.meanRate(),
meter.oneMinuteRate(),
meter.fiveMinuteRate(),
meter.fifteenMinuteRate(),
'second'
]
);
};

CsvReporter.prototype.reportTimer = function(timer, timestamp) {
var write = this.write.bind(this);
var percentiles = timer.percentiles([.50,.75,.95,.98,.99,.999]);

write(timestamp, timer.name,
'count,max,mean,min,stddev,p50,p75,p95,p98,p99,p999,mean_rate,m1_rate,' +
'm5_rate,m15_rate,rate_unit,duration_unit',
'%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,calls/%s,%s', [
timer.count(),
timer.max(),
timer.mean(),
timer.min(),
timer.stdDev(),
percentiles[.50],
percentiles[.75],
percentiles[.95],
percentiles[.98],
percentiles[.99],
percentiles[.999],
timer.meanRate(),
timer.oneMinuteRate(),
timer.fiveMinuteRate(),
timer.fifteenMinuteRate(),
'second',
'millisecond'
]);
};

module.exports = CsvReporter;
135 changes: 135 additions & 0 deletions reporting/graphite-reporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
'use strict';
var ScheduledReporter = require('./scheduled-reporter.js'),
util = require('util'),
Socket = require('net').Socket;

var reconnecting = false;

/**
* A custom reporter that sends metrics to a graphite server on the carbon tcp interface.
* @param {Report} registry report instance whose metrics to report on.
* @param {String} prefix A string to prefix on each metric (i.e. app.hostserver)
* @param {String} host The ip or hostname of the target graphite server.
* @param {String} port The port graphite is running on, defaults to 2003 if not specified.
* @constructor
*/
function GraphiteReporter(registry, prefix, host, port) {
GraphiteReporter.super_.call(this, registry);
this.prefix = prefix;
this.host = host;
this.port = port || 2003;
}

util.inherits(GraphiteReporter, ScheduledReporter);

GraphiteReporter.prototype.start = function(intervalInMs) {
var self = this;
this.socket = new Socket();
this.socket.on('error', function(exc) {
if(!reconnecting) {
reconnecting = true;
self.emit('log', 'warn', util.format('Lost connection to %s. Will reconnect in 10 seconds.', self.host), exc);
// Stop the reporter and try again in 1 second.
self.stop();
setTimeout(function () {
reconnecting = false;
self.start(intervalInMs);
}, 10000);
}
});

self.emit('log', 'verbose', util.format("Connecting to graphite @ %s:%d", this.host, this.port));
this.socket.connect(this.port, this.host, function() {
self.emit('log', 'verbose', util.format('Successfully connected to graphite @ %s:%d.', self.host, self.port));
GraphiteReporter.super_.prototype.start.call(self, intervalInMs);
});
};

GraphiteReporter.prototype.stop = function() {
GraphiteReporter.super_.prototype.stop.call(this);
this.socket.end();
};

GraphiteReporter.prototype.report = function() {
// Don't report while reconnecting.
if(reconnecting) {
return;
}
var metrics = this.getMetrics();
var self = this;
var timestamp = (new Date).getTime() / 1000;

if(metrics.counters.length != 0) {
metrics.counters.forEach(function (count) {
self.reportCounter.bind(self)(count, timestamp);
})
}

if(metrics.meters.length != 0) {
metrics.meters.forEach(function (meter) {
self.reportMeter.bind(self)(meter, timestamp);
})
}

if(metrics.timers.length != 0) {
metrics.timers.forEach(function (timer) {
// Don't log timer if its recorded no metrics.
if(timer.min() != null) {
self.reportTimer.bind(self)(timer, timestamp);
}
})
}
};

GraphiteReporter.prototype.send = function(name, value, timestamp) {
if(reconnecting) {
return;
}
this.socket.write(util.format('%s.%s %s %s\n', this.prefix, name, value,
timestamp));
};

GraphiteReporter.prototype.reportCounter = function(counter, timestamp) {
var send = this.send.bind(this);

send(counter.name, counter.count, timestamp);
};

GraphiteReporter.prototype.reportMeter = function(meter, timestamp) {
var send = this.send.bind(this);

send(util.format('%s.%s', meter.name, 'count'), meter.count, timestamp);
send(util.format('%s.%s', meter.name, 'mean_rate'), meter.meanRate(), timestamp);
send(util.format('%s.%s', meter.name, 'm1_rate'), meter.oneMinuteRate(),
timestamp);
send(util.format('%s.%s', meter.name, 'm5_rate'), meter.fiveMinuteRate(),
timestamp);
send(util.format('%s.%s', meter.name, 'm15_rate'), meter.fifteenMinuteRate(),
timestamp);
};

GraphiteReporter.prototype.reportTimer = function(timer, timestamp) {
var send = this.send.bind(this);
send(util.format('%s.%s', timer.name, 'count'), timer.count(), timestamp);
send(util.format('%s.%s', timer.name, 'mean_rate'), timer.meanRate(), timestamp);
send(util.format('%s.%s', timer.name, 'm1_rate'), timer.oneMinuteRate(),
timestamp);
send(util.format('%s.%s', timer.name, 'm5_rate'), timer.fiveMinuteRate(),
timestamp);
send(util.format('%s.%s', timer.name, 'm15_rate'), timer.fifteenMinuteRate(),
timestamp);

var percentiles = timer.percentiles([.50,.75,.95,.98,.99,.999]);
send(util.format('%s.%s', timer.name, 'min'), timer.min(), timestamp);
send(util.format('%s.%s', timer.name, 'mean'), timer.mean(), timestamp);
send(util.format('%s.%s', timer.name, 'max'), timer.max(), timestamp);
send(util.format('%s.%s', timer.name, 'stddev'), timer.stdDev(), timestamp);
send(util.format('%s.%s', timer.name, 'p50'), percentiles[.50], timestamp);
send(util.format('%s.%s', timer.name, 'p75'), percentiles[.75], timestamp);
send(util.format('%s.%s', timer.name, 'p95'), percentiles[.95], timestamp);
send(util.format('%s.%s', timer.name, 'p98'), percentiles[.98], timestamp);
send(util.format('%s.%s', timer.name, 'p99'), percentiles[.99], timestamp);
send(util.format('%s.%s', timer.name, 'p999'), percentiles[.999], timestamp);
};

module.exports = GraphiteReporter;
4 changes: 4 additions & 0 deletions reporting/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
exports.Report = require('./report');
exports.Server = require('./server');
exports.ScheduledReporter = require('./scheduled-reporter');
exports.ConsoleReporter = require('./console-reporter');
exports.CsvReporter = require('./csv-reporter');
exports.GraphiteReporter = require('./graphite-reporter');
Loading

0 comments on commit 375e78d

Please sign in to comment.