Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Composition Engine #323

Open
wants to merge 2 commits into from

3 participants

@thiagocaiubi

Here at Chaordic we use Librato and Graphite. Librato for several reasons. As you all know Librato can't compose metrics using functions like Graphite (ex.: Graph Data > Apply Function > Calculate > Ratio). So we built an engine to compose metrics based on existing ones.
Metrics are sent to the composition engine before flushing it to backends. Compositions are calculate based on rules defined at a source file. For example:

Rule:

{
      name: "#0.ctr.#1",
      regexp:[
        /^([a-z]+)\.click\.([a-z]+)/,
        /^([a-z]+)\.view\.([a-z]+)/
      ],
      compose: function(click, view) {
        return 100 * (click / view);
      }
}

Source Metrics:

{
    counters: {
        "site.click.page": 10,
        "site.view.page": 100
    }
}

The engine will store it matching metric to an array [10, 100] where 10 is the first matching regexp and 100 is the second matching regexp. After all matches in metrics the engine starting applying collected metrics to user-defined compose functions. So it will become:

click = 10;
view = 100;

Applied to:

compose: function(click, view) {
    return 100 * (click / view);
}

Will result: 10

The output compose metric will be named like name attribute defined in the rule object. The sharps are back reference to the matching strings, so:

#0 = site
#1 = page

Then, output metric will be:

site.ctr.page = 10

The final metrics object to be flushed to backends will look like:

{
    counters: {
        "site.click.page": 10,
        "site.view.page": 100,
        "site.ctr.page": 100
    }
}

Cheers!

@mrtazz
Owner

This looks really interesting, thanks for sending a pull request! From reading the code it looks like the engine works on the processed metrics. I'm wondering if it would make more sense to have it as a backend instead of hooking into the flush event?

@mheffner

@mrtazz I think this would need to be a processing stage between the flush event and the backends. Maybe we could add a plugin API for transforming metrics? The plugin would fire at flush time, but before the backends fire. Composite metrics would appear just as normal metrics to backends, requiring zero change to existing backends.

@mrtazz
Owner

Yeah I guess you're right. It would be even better if we find a way to have that plugin architecture in the metrics processing module itself. That way we would be able to do it in a single pass over the metrics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jul 29, 2013
  1. @thiagocaiubi

    Add a composition engine

    thiagocaiubi authored
Commits on Jul 30, 2013
  1. @thiagocaiubi
This page is out of date. Refresh to see the latest.
View
22 exampleConfig.js
@@ -87,6 +87,28 @@ Optional Variables:
[ { metric: 'foo', bins: [] },
{ metric: '', bins: [ 50, 100, 150, 200, 'inf'] } ]
+ compositions: rules to use while composing counters metrics.
+ source: path for the rules file. should comply to the following structure:
+ module.exports = [{
+ name: "#0.ctr.#1", // output metric. sharps are back references for following regexp
+ regexp: [
+ /^([a-z]+)\.click\.([a-z]+)/, // click metric regex
+ /^([a-z]+)\.view\.([a-z]+)/ // view metric regex
+ ],
+ compose: function(click, view) { // composing function
+ return 100 * (click / view);
+ }
+ },{
+ name: "#0.res_time.#1",
+ regexp: [
+ /^([a-z]+)\.time\.([a-z]+)/,
+ /^([a-z]+)\.count\.([a-z]+)/
+ ],
+ compose: function(time, count) {
+ return time / count;
+ }
+ }}];
+
*/
{
graphitePort: 2003
View
42 lib/composition_engine.js
@@ -0,0 +1,42 @@
+module.exports = function(compositions, sourceMetrics, callback) {
+ Object.keys(sourceMetrics.counters).forEach(function(key){
+ compositions.forEach(function(composition) {
+ composition.regexp.forEach(function(metric, index) {
+ var matches = key.match(metric),
+ metricName = composition.name;
+
+ if (!composition["metrics"]) {
+ composition.metrics = {};
+ }
+
+ if (matches) {
+ matches.slice(1, matches.length).forEach(function(m, index){
+ metricName = metricName.replace("#" + index, m);
+ });
+
+ if(!composition.metrics[metricName]) {
+ composition.metrics[metricName] = [];
+ }
+
+ composition.metrics[metricName][index] = sourceMetrics.counters[key];
+ }
+ });
+ });
+ });
+
+ compositions.forEach(function(composition){
+ Object.keys(composition.metrics).forEach(function(metric){
+ var result = composition.compose.apply(composition.compose, composition.metrics[metric]);
+
+ if (Number.isNaN(result)) {
+ return;
+ }
+
+ sourceMetrics.counters[metric] = result;
+ });
+
+ composition.metrics = {};
+ });
+
+ callback(sourceMetrics);
+};
View
13 stats.js
@@ -9,7 +9,8 @@ var dgram = require('dgram')
, logger = require('./lib/logger')
, set = require('./lib/set')
, pm = require('./lib/process_metrics')
- , mgmt = require('./lib/mgmt_console');
+ , mgmt = require('./lib/mgmt_console')
+ , ce = require('./lib/composition_engine');
// initialize data structures with defaults for statsd stats
@@ -120,7 +121,15 @@ function flushMetrics() {
});
pm.process_metrics(metrics_hash, flushInterval, time_stamp, function emitFlush(metrics) {
- backendEvents.emit('flush', time_stamp, metrics);
+ if (conf.compositions && conf.compositions.source) {
+
+ var compositions = require(conf.compositions.source);
+ ce(compositions, metrics, function(m) {
+ backendEvents.emit('flush', time_stamp, m);
+ });
+ } else {
+ backendEvents.emit('flush', time_stamp, metrics);
+ }
});
}
View
43 test/composition_engine_tests.js
@@ -0,0 +1,43 @@
+var ce = require('../lib/composition_engine');
+
+module.exports = {
+ setUp: function (callback) {
+
+ var counters = {};
+
+ this.metrics = {
+ counters: counters
+ }
+
+ this.compositions = [{
+ name: "#0.res_time.#1",
+ regexp:[
+ /^([a-z]+)\.time\.([a-z]+)/,
+ /^([a-z]+)\.count\.([a-z]+)/
+ ],
+ compose: function(time, count) {
+ return time / count;
+ }
+ }];
+
+ callback();
+ },
+ calculate_response_time: function(test) {
+ test.expect(1);
+ this.metrics.counters['site.time.page'] = 100;
+ this.metrics.counters['site.count.page'] = 2;
+ ce(this.compositions, this.metrics, function(metrics){
+ test.equal(50, metrics.counters['site.res_time.page']);
+ });
+ test.done();
+ },
+ should_not_store_NaN: function(test) {
+ test.expect(1);
+ this.metrics.counters['site.time.page'] = 100;
+ this.metrics.counters['site.count.page'] = undefined;
+ ce(this.compositions, this.metrics, function(metrics){
+ test.ok(!metrics.counters.hasOwnProperty['site.res_time.page']);
+ });
+ test.done();
+ }
+};
Something went wrong with that request. Please try again.