Prechádzať zdrojové kódy

Merge branch 'graphite-seriesbytag' of https://github.com/alexanderzobnin/grafana into alexanderzobnin-graphite-seriesbytag

Torkel Ödegaard 8 rokov pred
rodič
commit
4c8310c2bf

+ 54 - 0
public/app/plugins/datasource/graphite/datasource.ts

@@ -217,6 +217,60 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
     });
   };
 
+  this.getTags = function(optionalOptions) {
+    let options = optionalOptions || {};
+
+    let httpOptions: any =  {
+      method: 'GET',
+      url: '/tags',
+      // for cancellations
+      requestId: options.requestId,
+    };
+
+    if (options && options.range) {
+      httpOptions.params.from = this.translateTime(options.range.from, false);
+      httpOptions.params.until = this.translateTime(options.range.to, true);
+    }
+
+    return this.doGraphiteRequest(httpOptions).then(results => {
+      return _.map(results.data, tag => {
+        return {
+          text: tag.tag,
+          id: tag.id
+        };
+      });
+    });
+  };
+
+  this.getTagValues = function(tag, optionalOptions) {
+    let options = optionalOptions || {};
+
+    let httpOptions: any =  {
+      method: 'GET',
+      url: '/tags/' + tag,
+      // for cancellations
+      requestId: options.requestId,
+    };
+
+    if (options && options.range) {
+      httpOptions.params.from = this.translateTime(options.range.from, false);
+      httpOptions.params.until = this.translateTime(options.range.to, true);
+    }
+
+    return this.doGraphiteRequest(httpOptions).then(results => {
+      if (results.data && results.data.values) {
+        return _.map(results.data.values, value => {
+          return {
+            text: value.value,
+            id: value.id
+          };
+        });
+      } else {
+        return [];
+      }
+    });
+  };
+
   this.testDatasource = function() {
     return this.metricFindQuery('*').then(function () {
       return { status: "success", message: "Data source is working"};

+ 27 - 1
public/app/plugins/datasource/graphite/partials/query.editor.html

@@ -6,12 +6,38 @@
 
   <div ng-hide="ctrl.target.textEditor">
 		<div class="gf-form-inline">
+      <div class="gf-form" ng-if="ctrl.seriesByTagUsed">
+        <label class="gf-form-label query-keyword">seriesByTag</label>
+      </div>
+
+      <div ng-repeat="tag in ctrl.tags" class="gf-form">
+        <gf-form-dropdown model="tag.key" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-key"
+          get-options="ctrl.getTags()"
+          on-change="ctrl.tagChanged(tag, $index)">
+        </gf-form-dropdown>
+        <gf-form-dropdown model="tag.operator" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-operator"
+          get-options="ctrl.getTagOperators()"
+          on-change="ctrl.tagChanged(tag, $index)">
+        </gf-form-dropdown>
+        <gf-form-dropdown model="tag.value" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-value"
+          get-options="ctrl.getTagValues(tag)"
+          on-change="ctrl.tagChanged(tag, $index)">
+        </gf-form-dropdown>
+        <label class="gf-form-label query-keyword" ng-if="ctrl.showDelimiter($index)">,</label>
+      </div>
+      <div ng-if="ctrl.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form">
+        <metric-segment segment="segment"
+          get-options="ctrl.getTagsAsSegments()"
+          on-change="ctrl.addNewTag(segment)">
+        </metric-segment>
+      </div>
+
       <div ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form">
         <metric-segment segment="segment" get-options="ctrl.getAltSegments($index)" on-change="ctrl.segmentValueChanged(segment, $index)"></metric-segment>
       </div>
 
       <div ng-repeat="func in ctrl.functions" class="gf-form">
-        <span graphite-func-editor class="gf-form-label query-part"></span>
+        <span graphite-func-editor class="gf-form-label query-part" ng-hide="ctrl.getSeriesByTagFuncIndex() === $index"></span>
       </div>
 
       <div class="gf-form dropdown">

+ 135 - 0
public/app/plugins/datasource/graphite/query_ctrl.ts

@@ -7,11 +7,17 @@ import {Parser} from './parser';
 import {QueryCtrl} from 'app/plugins/sdk';
 import appEvents from 'app/core/app_events';
 
+const GRAPHITE_TAG_OPERATORS = ['=', '!=', '=~', '!=~'];
+
 export class GraphiteQueryCtrl extends QueryCtrl {
   static templateUrl = 'partials/query.editor.html';
 
   functions: any[];
   segments: any[];
+  addTagSegments: any[];
+  tags: any[];
+  seriesByTagUsed: boolean;
+  removeTagValue: string;
 
   /** @ngInject **/
   constructor($scope, $injector, private uiSegmentSrv, private templateSrv) {
@@ -21,6 +27,8 @@ export class GraphiteQueryCtrl extends QueryCtrl {
       this.target.target = this.target.target || '';
       this.parseTarget();
     }
+
+    this.removeTagValue = '-- remove tag --';
   }
 
   toggleEditorMode() {
@@ -59,6 +67,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
     }
 
     this.checkOtherSegments(this.segments.length - 1);
+    this.checkForSeriesByTag();
   }
 
   addFunctionParameter(func, value, index, shiftBack) {
@@ -304,6 +313,10 @@ export class GraphiteQueryCtrl extends QueryCtrl {
     if (!newFunc.params.length && newFunc.added) {
       this.targetChanged();
     }
+
+    if (newFunc.def.name === 'seriesByTag') {
+      this.parseTarget();
+    }
   }
 
   moveAliasFuncLast() {
@@ -333,4 +346,126 @@ export class GraphiteQueryCtrl extends QueryCtrl {
       }
     }
   }
+
+  //////////////////////////////////
+  // Graphite seriesByTag support //
+  //////////////////////////////////
+
+  checkForSeriesByTag() {
+    let seriesByTagFunc = _.find(this.functions, (func) => func.def.name === 'seriesByTag');
+    if (seriesByTagFunc) {
+      this.seriesByTagUsed = true;
+      let tags = this.splitSeriesByTagParams(seriesByTagFunc);
+      this.tags = tags;
+      this.fixTagSegments();
+    }
+  }
+
+  splitSeriesByTagParams(func) {
+    const tagPattern = /([^\!=~]+)([\!=~]+)([^\!=~]+)/;
+    return _.flatten(_.map(func.params, (param: string) => {
+      let matches = tagPattern.exec(param);
+      if (matches) {
+        let tag = matches.slice(1);
+        if (tag.length === 3) {
+          return {
+            key: tag[0],
+            operator: tag[1],
+            value: tag[2]
+          }
+        }
+      }
+      return [];
+    }));
+  }
+
+  getTags() {
+    return this.datasource.getTags().then((values) => {
+      let altTags = _.map(values, 'text');
+      altTags.splice(0, 0, this.removeTagValue);
+      return mapToDropdownOptions(altTags);
+    });
+  }
+
+  getTagsAsSegments() {
+    return this.datasource.getTags().then((values) => {
+      return _.map(values, (val) => {
+        return this.uiSegmentSrv.newSegment(val.text);
+      });
+    });
+  }
+
+  getTagOperators() {
+    return mapToDropdownOptions(GRAPHITE_TAG_OPERATORS);
+  }
+
+  getTagValues(tag) {
+    let tagKey = tag.key;
+    return this.datasource.getTagValues(tagKey).then((values) => {
+      let altValues = _.map(values, 'text');
+      return mapToDropdownOptions(altValues);
+    });
+  }
+
+  tagChanged(tag, tagIndex) {
+    this.error = null;
+
+    if (tag.key === this.removeTagValue) {
+      this.removeTag(tagIndex);
+      return;
+    }
+
+    let newTagParam = renderTagString(tag);
+    this.getSeriesByTagFunc().params[tagIndex] = newTagParam;
+    this.tags[tagIndex] = tag;
+    this.targetChanged();
+  }
+
+  getSeriesByTagFuncIndex() {
+    return _.findIndex(this.functions, (func) => func.def.name === 'seriesByTag');
+  }
+
+  getSeriesByTagFunc() {
+    let seriesByTagFuncIndex = this.getSeriesByTagFuncIndex();
+    if (seriesByTagFuncIndex >= 0) {
+      return this.functions[seriesByTagFuncIndex];
+    } else {
+      return undefined;
+    }
+  }
+
+  addNewTag(segment) {
+    let newTagKey = segment.value;
+    let newTag = {key: newTagKey, operator: '=', value: 'select tag value'};
+    let newTagParam = renderTagString(newTag);
+    this.getSeriesByTagFunc().params.push(newTagParam);
+    this.tags.push(newTag);
+    this.targetChanged();
+    this.fixTagSegments();
+  }
+
+  removeTag(index) {
+    this.getSeriesByTagFunc().params.splice(index, 1);
+    this.tags.splice(index, 1);
+    this.targetChanged();
+  }
+
+  fixTagSegments() {
+    // Adding tag with the same name as just removed works incorrectly if single segment is used (instead of array)
+    this.addTagSegments = [this.uiSegmentSrv.newPlusButton()];
+  }
+
+  showDelimiter(index) {
+    return index !== this.tags.length - 1;
+  }
+}
+
+function renderTagString(tag) {
+  return tag.key + tag.operator + tag.value;
+}
+
+function mapToDropdownOptions(results) {
+  return _.map(results, (value) => {
+    return {text: value, value: value};
+  });
 }

+ 109 - 0
public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts

@@ -210,4 +210,113 @@ describe('GraphiteQueryCtrl', function() {
     });
   });
 
+  describe('when adding seriesByTag function', function() {
+    beforeEach(function() {
+      ctx.ctrl.target.target = '';
+      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
+      ctx.ctrl.parseTarget();
+      ctx.ctrl.addFunction(gfunc.getFuncDef('seriesByTag'));
+    });
+
+    it('should update functions', function() {
+      expect(ctx.ctrl.getSeriesByTagFuncIndex()).to.be(0);
+    });
+
+    it('should update seriesByTagUsed flag', function() {
+      expect(ctx.ctrl.seriesByTagUsed).to.be(true);
+    });
+
+    it('should update target', function() {
+      expect(ctx.ctrl.target.target).to.be('seriesByTag()');
+    });
+
+    it('should call refresh', function() {
+      expect(ctx.panelCtrl.refresh.called).to.be(true);
+    });
+  });
+
+  describe('when parsing seriesByTag function', function() {
+    beforeEach(function() {
+      ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
+      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
+      ctx.ctrl.parseTarget();
+    });
+
+    it('should add tags', function() {
+      const expected = [
+        {key: 'tag1', operator: '=', value: 'value1'},
+        {key: 'tag2', operator: '!=~', value: 'value2'}
+      ];
+      expect(ctx.ctrl.tags).to.eql(expected);
+    });
+
+    it('should add plus button', function() {
+      expect(ctx.ctrl.addTagSegments.length).to.be(1);
+    });
+  });
+
+  describe('when tag added', function() {
+    beforeEach(function() {
+      ctx.ctrl.target.target = "seriesByTag()";
+      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
+      ctx.ctrl.parseTarget();
+      ctx.ctrl.addNewTag({value: 'tag1'});
+    });
+
+    it('should update tags with default value', function() {
+      const expected = [
+        {key: 'tag1', operator: '=', value: 'select tag value'}
+      ];
+      expect(ctx.ctrl.tags).to.eql(expected);
+    });
+
+    it('should update target', function() {
+      const expected = "seriesByTag('tag1=select tag value')";
+      expect(ctx.ctrl.target.target).to.eql(expected);
+    });
+  });
+
+  describe('when tag changed', function() {
+    beforeEach(function() {
+      ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
+      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
+      ctx.ctrl.parseTarget();
+      ctx.ctrl.tagChanged({key: 'tag1', operator: '=', value: 'new_value'}, 0);
+    });
+
+    it('should update tags', function() {
+      const expected = [
+        {key: 'tag1', operator: '=', value: 'new_value'},
+        {key: 'tag2', operator: '!=~', value: 'value2'}
+      ];
+      expect(ctx.ctrl.tags).to.eql(expected);
+    });
+
+    it('should update target', function() {
+      const expected = "seriesByTag('tag1=new_value', 'tag2!=~value2')";
+      expect(ctx.ctrl.target.target).to.eql(expected);
+    });
+  });
+
+  describe('when tag removed', function() {
+    beforeEach(function() {
+      ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
+      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
+      ctx.ctrl.parseTarget();
+      ctx.ctrl.removeTag(0);
+    });
+
+    it('should update tags', function() {
+      const expected = [
+        {key: 'tag2', operator: '!=~', value: 'value2'}
+      ];
+      expect(ctx.ctrl.tags).to.eql(expected);
+    });
+
+    it('should update target', function() {
+      const expected = "seriesByTag('tag2!=~value2')";
+      expect(ctx.ctrl.target.target).to.eql(expected);
+    });
+  });
+
 });