Ver código fonte

Graphite: Graphite query builder can now handle functions that multiple series as arguments! #117

Torkel Ödegaard 11 anos atrás
pai
commit
9f60745e57

+ 1 - 0
CHANGELOG.md

@@ -10,6 +10,7 @@
 - [Issue #219](https://github.com/grafana/grafana/issues/219). Templating: Template variable value selection is now a typeahead autocomplete dropdown
 
 **New features and improvements**
+- [Issue #117](https://github.com/grafana/grafana/issues/117). Graphite: Graphite query builder can now handle functions that multiple series as arguments!
 - [Issue #281](https://github.com/grafana/grafana/issues/281). Graphite: Metric node/segment selection is now a textbox with autocomplete dropdown, allow for custom glob expression for single node segment without entering text editor mode.
 - [Issue #578](https://github.com/grafana/grafana/issues/578). Dashboard: Row option to display row title even when the row is visible
 - [Issue #672](https://github.com/grafana/grafana/issues/672). Dashboard: panel fullscreen & edit state is present in url, can now link to graph in edit & fullscreen mode.

+ 19 - 6
src/app/controllers/graphiteTarget.js

@@ -9,11 +9,13 @@ function (angular, _, config, gfunc, Parser) {
   'use strict';
 
   var module = angular.module('grafana.controllers');
+  var targetLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'];
 
   module.controller('GraphiteTargetCtrl', function($scope, $sce, templateSrv) {
 
     $scope.init = function() {
       $scope.target.target = $scope.target.target || '';
+      $scope.targetLetter = targetLetters[$scope.$index];
 
       parseTarget();
     };
@@ -69,6 +71,14 @@ function (angular, _, config, gfunc, Parser) {
         $scope.functions.push(innerFunc);
         break;
 
+      case 'series-ref':
+        if ($scope.segments.length === 0) {
+          func.params[index] = astNode.value;
+        }
+        else {
+          func.params[index - 1] = astNode.value;
+        }
+        break;
       case 'string':
       case 'number':
         if ((index-1) >= func.def.params.length) {
@@ -81,9 +91,7 @@ function (angular, _, config, gfunc, Parser) {
         else {
           func.params[index - 1] = astNode.value;
         }
-
         break;
-
       case 'metric':
         if ($scope.segments.length > 0) {
           throw { message: 'Multiple metric params not supported, use text editor.' };
@@ -113,8 +121,10 @@ function (angular, _, config, gfunc, Parser) {
       return $scope.datasource.metricFindQuery(path)
         .then(function(segments) {
           if (segments.length === 0) {
-            $scope.segments = $scope.segments.splice(0, fromIndex);
-            $scope.segments.push(new MetricSegment('select metric'));
+            if (path !== '') {
+              $scope.segments = $scope.segments.splice(0, fromIndex);
+              $scope.segments.push(new MetricSegment('select metric'));
+            }
             return;
           }
           if (segments[0].expandable) {
@@ -144,8 +154,7 @@ function (angular, _, config, gfunc, Parser) {
     $scope.getAltSegments = function (index) {
       $scope.altSegments = [];
 
-      var query = index === 0 ?
-        '*' : getSegmentPathUpTo(index) + '.*';
+      var query = index === 0 ?  '*' : getSegmentPathUpTo(index) + '.*';
 
       return $scope.datasource.metricFindQuery(query)
         .then(function(segments) {
@@ -226,6 +235,10 @@ function (angular, _, config, gfunc, Parser) {
       if (!newFunc.params.length && newFunc.added) {
         $scope.targetChanged();
       }
+
+      if ($scope.segments.length === 1 && $scope.segments[0].value === 'select metric') {
+        $scope.segments = [];
+      }
     };
 
     $scope.moveAliasFuncLast = function() {

+ 1 - 3
src/app/directives/graphiteFuncEditor.js

@@ -69,7 +69,6 @@ function (angular, _, $) {
 
           function inputBlur(paramIndex) {
             /*jshint validthis:true */
-
             var $input = $(this);
             var $link = $input.prev();
 
@@ -88,7 +87,6 @@ function (angular, _, $) {
 
           function inputKeyPress(paramIndex, e) {
             /*jshint validthis:true */
-
             if(e.which === 13) {
               inputBlur.call(this, paramIndex);
             }
@@ -147,7 +145,7 @@ function (angular, _, $) {
             $funcLink.appendTo(elem);
 
             _.each(funcDef.params, function(param, index) {
-              if (param.optional && func.params.length !== index + 1) {
+              if (param.optional && func.params.length <= index) {
                 return;
               }
 

+ 1 - 4
src/app/directives/graphiteSegment.js

@@ -64,10 +64,7 @@ function (angular, app, _, $) {
           };
 
           $scope.source = function(query, callback) {
-            console.log("source!", callback);
-            if (options) {
-              return options;
-            }
+            if (options) { return options; }
 
             $scope.$apply(function() {
               $scope.getAltSegments($scope.$index).then(function() {

+ 4 - 3
src/app/partials/graphite/editor.html

@@ -47,6 +47,9 @@
       </ul>
 
       <ul class="grafana-target-controls-left">
+				<li class="grafana-target-segment" style="min-width: 15px; text-align: center">
+					{{targetLetter}}
+        </li>
         <li>
           <a  class="grafana-target-segment"
               ng-click="target.hide = !target.hide; get_data();"
@@ -65,9 +68,7 @@
               ng-show="showTextEditor" />
 
       <ul class="grafana-segment-list" role="menu" ng-hide="showTextEditor">
-        <li ng-repeat="segment in segments" role="menuitem" graphite-segment>
-
-        </li>
+        <li ng-repeat="segment in segments" role="menuitem" graphite-segment></li>
 				<li ng-repeat="func in functions">
           <span graphite-func-editor class="grafana-target-segment grafana-target-function">
           </span>

+ 13 - 3
src/app/services/graphite/gfunc.js

@@ -59,13 +59,23 @@ function (_) {
 
   addFuncDef({
     name: 'diffSeries',
+    params: [
+      { name: 'other', type: 'value_or_series', optional: true },
+      { name: 'other', type: 'value_or_series', optional: true },
+      { name: 'other', type: 'value_or_series', optional: true }
+    ],
+    defaultParams: ['$B'],
     category: categories.Calculate,
   });
 
   addFuncDef({
     name: 'asPercent',
-    params: [{ name: 'other', type: 'value_or_series', optional: true }],
-    defaultParams: ['$B'],
+    params: [
+      { name: 'other', type: 'value_or_series', optional: true },
+      { name: 'other', type: 'value_or_series', optional: true },
+      { name: 'other', type: 'value_or_series', optional: true }
+    ],
+    defaultParams: ['#A'],
     category: categories.Calculate,
   });
 
@@ -508,7 +518,7 @@ function (_) {
 
     }, this);
 
-    if (metricExp !== undefined) {
+    if (metricExp) {
       parameters.unshift(metricExp);
     }
 

+ 36 - 17
src/app/services/graphite/graphiteDatasource.js

@@ -210,31 +210,50 @@ function (angular, _, $, config, kbn, moment) {
       return $http(options);
     };
 
+    GraphiteDatasource.prototype._seriesRefLetters = [
+      '#A', '#B', '#C', '#D',
+      '#E', '#F', '#G', '#H',
+      '#I', '#J', '#K', '#L',
+      '#M', '#N', '#O'
+    ];
+
     GraphiteDatasource.prototype.buildGraphiteParams = function(options) {
-      var clean_options = [];
-      var graphite_options = ['target', 'targets', 'from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
+      var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
+      var clean_options = [], targets = {};
+      var target, targetValue, i;
+      var regex = /(\#[A-Z])/g;
 
       if (options.format !== 'png') {
         options['format'] = 'json';
       }
 
-      _.each(options, function (value, key) {
-        if ($.inArray(key, graphite_options) === -1) {
-          return;
-        }
+      for (i = 0; i < options.targets.length; i++) {
+        target = options.targets[i];
+        targetValue = templateSrv.replace(target.target);
+        targets[this._seriesRefLetters[i]] = targetValue;
+      }
 
-        if (key === "targets") {
-          _.each(value, function (value) {
-            if (value.target && !value.hide) {
-              var targetValue = templateSrv.replace(value.target);
-              clean_options.push("target=" + encodeURIComponent(targetValue));
-            }
-          }, this);
-        }
-        else if (value) {
-          clean_options.push(key + "=" + encodeURIComponent(value));
+      function nestedSeriesRegexReplacer(match) {
+        return targets[match];
+      }
+
+      for (i = 0; i < options.targets.length; i++) {
+        target = options.targets[i];
+        if (!target.target || target.hide) {
+          continue;
         }
-      }, this);
+
+        targetValue = targets[this._seriesRefLetters[i]];
+        targetValue = targetValue.replace(regex, nestedSeriesRegexReplacer);
+
+        clean_options.push("target=" + encodeURIComponent(targetValue));
+      }
+
+      _.each(options, function (value, key) {
+        if ($.inArray(key, graphite_options) === -1) { return; }
+        clean_options.push(key + "=" + encodeURIComponent(value));
+      });
+
       return clean_options;
     };
 

+ 1 - 0
src/app/services/graphite/lexer.js

@@ -128,6 +128,7 @@ define([
       i === 93 ||           // templateEnd ]
       i === 63 ||           // ?
       i === 37 ||           // %
+      i === 35 ||           // #
       i >= 97 && i <= 122;  // a-z
   }
 

+ 19 - 0
src/app/services/graphite/parser.js

@@ -157,6 +157,7 @@ define([
       var param =
         this.functionCall() ||
         this.numericLiteral() ||
+        this.seriesRefExpression() ||
         this.metricExpression() ||
         this.stringLiteral();
 
@@ -168,6 +169,24 @@ define([
       return [param].concat(this.functionParameters());
     },
 
+    seriesRefExpression: function() {
+      if (!this.match('identifier')) {
+        return null;
+      }
+
+      var value = this.tokens[this.index].value;
+      if (!value.match(/\#[A-Z]/)) {
+        return null;
+      }
+
+      var token = this.consumeToken();
+
+      return {
+        type: 'series-ref',
+        value: token.value
+      };
+    },
+
     numericLiteral: function () {
       if (!this.match('number')) {
         return null;

+ 3 - 1
src/css/less/grafana.less

@@ -211,8 +211,10 @@ input[type=text].grafana-function-param-input {
 .grafana-target-controls-left {
   list-style: none;
   float: left;
-  width: 30px;
   margin: 0px;
+  li {
+    display: inline-block;
+  }
 }
 
 .grafana-target-controls {

+ 84 - 0
src/test/specs/graphiteDatasource-specs.js

@@ -0,0 +1,84 @@
+define([
+  './helpers',
+  'services/graphite/graphiteDatasource'
+], function(helpers) {
+  'use strict';
+
+  describe('graphiteDatasource', function() {
+    var ctx = new helpers.ServiceTestContext();
+
+    beforeEach(module('grafana.services'));
+    beforeEach(ctx.providePhase());
+    beforeEach(ctx.createService('GraphiteDatasource'));
+    beforeEach(function() {
+      ctx.ds = new ctx.service({ url: [''] });
+    });
+
+    describe('When querying influxdb with one target using query editor target spec', function() {
+      var query = {
+        range: { from: 'now-1h', to: 'now' },
+        targets: [{ target: 'prod1.count' }, {target: 'prod2.count'}],
+        maxDataPoints: 500
+      };
+
+      var response = [{ target: 'prod1.count', points: [[10, 1], [12,1]], }];
+      var results;
+      var request;
+
+      beforeEach(function() {
+
+        ctx.$httpBackend.expectPOST('/render', function(body) { request = body; return true; })
+          .respond(response);
+
+        ctx.ds.query(query).then(function(data) { results = data; });
+        ctx.$httpBackend.flush();
+      });
+
+      it('should generate the correct query', function() {
+        ctx.$httpBackend.verifyNoOutstandingExpectation();
+      });
+
+      it('should query correctly', function() {
+        var params = request.split('&');
+        expect(params).to.contain('target=prod1.count');
+        expect(params).to.contain('target=prod2.count');
+        expect(params).to.contain('from=-1h');
+        expect(params).to.contain('until=now');
+      });
+
+      it('should return series list', function() {
+        expect(results.data.length).to.be(1);
+        expect(results.data[0].target).to.be('prod1.count');
+      });
+
+    });
+
+    describe('building graphite params', function() {
+
+      it('should uri escape targets', function() {
+        var results = ctx.ds.buildGraphiteParams({
+          targets: [{target: 'prod1.{test,test2}'}, {target: 'prod2.count'}]
+        });
+        expect(results).to.contain('target=prod1.%7Btest%2Ctest2%7D');
+      });
+
+      it('should replace target placeholder', function() {
+        var results = ctx.ds.buildGraphiteParams({
+          targets: [{target: 'series1'}, {target: 'series2'}, {target: 'asPercent(#A,#B)'}]
+        });
+        expect(results[2]).to.be('target=asPercent(series1%2Cseries2)');
+      });
+
+      it('should ignore empty targets', function() {
+        var results = ctx.ds.buildGraphiteParams({
+          targets: [{target: 'series1'}, {target: ''}]
+        });
+        expect(results.length).to.be(2);
+      });
+
+    });
+
+  });
+
+});
+

+ 53 - 0
src/test/specs/graphiteTargetCtrl-specs.js

@@ -64,6 +64,59 @@ define([
       });
     });
 
+    describe('when adding function before any metric segment', function() {
+      beforeEach(function() {
+        ctx.scope.target.target = '';
+        ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: true}]));
+        ctx.scope.init();
+        ctx.scope.$digest();
+
+        ctx.scope.$parent = { get_data: sinon.spy() };
+        ctx.scope.addFunction(gfunc.getFuncDef('asPercent'));
+      });
+
+      it('should add function and remove select metric link', function() {
+        expect(ctx.scope.segments.length).to.be(0);
+      });
+    });
+
+    describe('when initalizing target without metric expression and only function', function() {
+      beforeEach(function() {
+        ctx.scope.target.target = 'asPercent(#A, #B)';
+        ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([]));
+        ctx.scope.init();
+        ctx.scope.$digest();
+        ctx.scope.$parent = { get_data: sinon.spy() };
+      });
+
+      it('should not add select metric segment', function() {
+        expect(ctx.scope.segments.length).to.be(0);
+      });
+
+      it('should add both series refs as params', function() {
+        expect(ctx.scope.functions[0].params.length).to.be(2);
+      });
+
+    });
+
+    describe('when initalizing target without metric expression and function with series-ref', function() {
+      beforeEach(function() {
+        ctx.scope.target.target = 'asPercent(metric.node.count, #A)';
+        ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([]));
+        ctx.scope.init();
+        ctx.scope.$digest();
+        ctx.scope.$parent = { get_data: sinon.spy() };
+      });
+
+      it('should add segments', function() {
+        expect(ctx.scope.segments.length).to.be(3);
+      });
+
+      it('should have correct func params', function() {
+        expect(ctx.scope.functions[0].params.length).to.be(1);
+      });
+    });
+
     describe('targetChanged', function() {
       beforeEach(function() {
         ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: false}]));

+ 10 - 0
src/test/specs/parser-specs.js

@@ -156,6 +156,16 @@ define([
       expect(rootNode.segments[1].value).to.be('test');
     });
 
+    it('series parameters', function() {
+      var parser = new Parser('asPercent(#A, #B)');
+      var rootNode = parser.getAst();
+      expect(rootNode.type).to.be('function');
+      expect(rootNode.params[0].type).to.be('series-ref');
+      expect(rootNode.params[0].value).to.be('#A');
+      expect(rootNode.params[1].value).to.be('#B');
+    });
+
+
   });
 
 });

+ 1 - 0
src/test/test-main.js

@@ -121,6 +121,7 @@ require([
     'specs/timeSeries-specs',
     'specs/row-ctrl-specs',
     'specs/graphiteTargetCtrl-specs',
+    'specs/graphiteDatasource-specs',
     'specs/influxSeries-specs',
     'specs/influxQueryBuilder-specs',
     'specs/influxdb-datasource-specs',