Explorar o código

Improvement and polish to the OpenTSDB query editor (Issue #492)

Torkel Ödegaard %!s(int64=11) %!d(string=hai) anos
pai
achega
6885cea0fd

+ 3 - 0
CHANGELOG.md

@@ -14,6 +14,9 @@
 - Improvement to InfluxDB query editor and function/value column selection (Issue #473)
 - Initial support for filtering (templated queries) for InfluxDB (PR #375) - thx @mavimo
 - Row editing and adding new panel is now a lot quicker and easier with the new row menu (Issue #475)
+- New datasource! Initial support for OpenTSDB (PR #211) - thx @mpage
+- Improvement and polish to the OpenTSDB query editor (Issue #492)
+
 
 #### Changes
 - Graphite panel is now renamed graph (Existing dashboards will still work)

+ 1 - 0
src/app/controllers/all.js

@@ -11,4 +11,5 @@ define([
   './influxTargetCtrl',
   './playlistCtrl',
   './inspectCtrl',
+  './opentsdbTargetCtrl',
 ], function () {});

+ 118 - 0
src/app/controllers/opentsdbTargetCtrl.js

@@ -0,0 +1,118 @@
+define([
+  'angular',
+  'underscore',
+  'kbn'
+],
+function (angular, _, kbn) {
+  'use strict';
+
+  var module = angular.module('kibana.controllers');
+
+  module.controller('OpenTSDBTargetCtrl', function($scope, $timeout) {
+
+    $scope.init = function() {
+      $scope.target.errors = validateTarget($scope.target);
+      $scope.aggregators = ['avg', 'sum', 'min', 'max', 'dev', 'zimsum', 'mimmin', 'mimmax'];
+
+      if (!$scope.target.aggregator) {
+        $scope.target.aggregator = 'sum';
+      }
+
+      if (!$scope.target.downsampleAggregator) {
+        $scope.target.downsampleAggregator = 'sum';
+      }
+
+      $scope.$on('typeahead-updated', function() {
+        $timeout($scope.targetBlur);
+      });
+    };
+
+    $scope.targetBlur = function() {
+      $scope.target.errors = validateTarget($scope.target);
+
+      // this does not work so good
+      if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) {
+        $scope.oldTarget = angular.copy($scope.target);
+        $scope.get_data();
+      }
+    };
+
+    $scope.duplicate = function() {
+      var clone = angular.copy($scope.target);
+      $scope.panel.targets.push(clone);
+    };
+
+    $scope.suggestMetrics = function(query, callback) {
+      $scope.datasource
+        .performSuggestQuery(query, 'metrics')
+        .then(callback);
+    };
+
+    $scope.suggestTagKeys = function(query, callback) {
+      $scope.datasource
+        .performSuggestQuery(query, 'tagk')
+        .then(callback);
+    };
+
+    $scope.suggestTagValues = function(query, callback) {
+      $scope.datasource
+        .performSuggestQuery(query, 'tagv')
+        .then(callback);
+    };
+
+    $scope.addTag = function() {
+      if (!$scope.addTagMode) {
+        $scope.addTagMode = true;
+        return;
+      }
+
+      if (!$scope.target.tags) {
+        $scope.target.tags = {};
+      }
+
+      $scope.target.errors = validateTarget($scope.target);
+
+      if (!$scope.target.errors.tags) {
+        $scope.target.tags[$scope.target.currentTagKey] = $scope.target.currentTagValue;
+        $scope.target.currentTagKey = '';
+        $scope.target.currentTagValue = '';
+        $scope.targetBlur();
+      }
+
+      $scope.addTagMode = false;
+    };
+
+    $scope.removeTag = function(key) {
+      delete $scope.target.tags[key];
+      $scope.targetBlur();
+    };
+
+    function validateTarget(target) {
+      var errs = {};
+
+      if (!target.metric) {
+        errs.metric = "You must supply a metric name.";
+      }
+
+      if (target.shouldDownsample) {
+        try {
+          if (target.downsampleInterval) {
+            kbn.describe_interval(target.downsampleInterval);
+          } else {
+            errs.downsampleInterval = "You must supply a downsample interval (e.g. '1m' or '1h').";
+          }
+        } catch(err) {
+          errs.downsampleInterval = err.message;
+        }
+      }
+
+      if (target.tags && _.has(target.tags, target.currentTagKey)) {
+        errs.tags = "Duplicate tag key '" + target.currentTagKey + "'.";
+      }
+
+      return errs;
+    }
+
+  });
+
+});

+ 322 - 0
src/app/partials/opentsdb/editor.html

@@ -0,0 +1,322 @@
+<div class="editor-row" style="margin-top: 10px;">
+  <div  ng-repeat="target in panel.targets"
+        class="grafana-target"
+        ng-class="{'grafana-target-hidden': target.hide}"
+        ng-controller="OpenTSDBTargetCtrl"
+        ng-init="init()">
+
+    <div class="grafana-target-inner-wrapper">
+      <div class="grafana-target-inner">
+        <ul class="grafana-target-controls">
+          <li class="dropdown">
+            <a  class="pointer dropdown-toggle"
+                data-toggle="dropdown"
+                tabindex="1">
+              <i class="icon-cog"></i>
+            </a>
+            <ul class="dropdown-menu pull-right" role="menu">
+              <li role="menuitem">
+                <a  tabindex="1"
+                    ng-click="duplicate()">
+                  Duplicate
+                </a>
+              </li>
+            </ul>
+          </li>
+          <li>
+            <a class="pointer" tabindex="1" ng-click="removeTarget(target)">
+              <i class="icon-remove"></i>
+            </a>
+          </li>
+        </ul>
+
+        <ul class="grafana-target-controls-left">
+          <li>
+            <a  class="grafana-target-segment"
+                ng-click="target.hide = !target.hide; get_data();"
+                role="menuitem">
+              <i class="icon-eye-open"></i>
+            </a>
+          </li>
+        </ul>
+
+        <ul class="grafana-segment-list" role="menu">
+          <li>
+            <input type="text"
+                   class="input-xxlarge grafana-target-segment-input"
+                   ng-model="target.metric"
+                   spellcheck='false'
+                   bs-typeahead="suggestMetrics"
+                   placeholder="metric name"
+                   data-min-length=0 data-items=100
+                   ng-blur="targetBlur()"
+                   >
+            <a bs-tooltip="target.errors.metric"
+               style="color: rgb(229, 189, 28)"
+               ng-show="target.errors.metric">
+              <i class="icon-warning-sign"></i>
+            </a>
+          </li>
+          <li class="grafana-target-segment">
+            Aggregator
+          <li>
+            <select ng-model="target.aggregator"
+                    class="grafana-target-segment-input input-small"
+                    ng-options="agg for agg in aggregators"
+                    ng-change="targetBlur()">
+            </select>
+            <a bs-tooltip="target.errors.aggregator"
+               style="color: rgb(229, 189, 28)"
+               ng-show="target.errors.aggregator">
+              <i class="icon-warning-sign"></i>
+            </a>
+          </li>
+
+          <li class="grafana-target-segment">
+              Rate:
+              <input type="checkbox"
+                     class="grafana-target-option-checkbox"
+                     ng-model="target.shouldComputeRate"
+                     ng-change="targetBlur()"
+                     >
+          </li>
+          <li class="grafana-target-segment" ng-hide="!target.shouldComputeRate">
+            Counter:
+            <input type="checkbox"
+                   class="grafana-target-option-checkbox"
+                   ng-disabled="!target.shouldComputeRate"
+                   ng-model="target.isCounter"
+                   ng-change="targetBlur()">
+          </li>
+        </ul>
+
+        <div class="clearfix"></div>
+      </div>
+
+      <div class="grafana-target-inner">
+        <ul class="grafana-segment-list" role="menu">
+
+          <li class="grafana-target-segment">
+            Downsample:
+            <input type="checkbox"
+                   class="grafana-target-option-checkbox"
+                   ng-model="target.shouldDownsample"
+                   ng-change="targetBlur(target)"
+                   >
+          </li>
+
+          <li ng-hide="!target.shouldDownsample">
+            <input type="text"
+                   class="input-small grafana-target-segment-input"
+                   ng-disabled="!target.shouldDownsample"
+                   ng-model="target.downsampleInterval"
+                   ng-change="targetBlur()"
+                   placeholder="interval"
+                   >
+          </li>
+
+          <li class="grafana-target-segment" ng-hide="!target.shouldDownsample">
+            Aggregator
+          </li>
+
+          <li ng-hide="!target.shouldDownsample">
+            <select ng-model="target.downsampleAggregator"
+                    class="grafana-target-segment-input input-small"
+                    ng-options="agg for agg in aggregators"
+                    ng-change="targetBlur()">
+            </select>
+          </li>
+
+          <li class="grafana-target-segment">
+              Tags:
+          </li>
+          <li ng-repeat="(key, value) in target.tags track by $index" class="grafana-target-segment">
+            {{key}}&nbsp;=&nbsp;{{value}}
+            <a ng-click="removeTag(key)">
+              <i class="icon-remove"></i>
+            </a>
+          </li>
+
+          <li class="grafana-target-segment" ng-hide="addTagMode">
+            <a ng-click="addTag()">
+              <i class="icon-plus-sign"></i>
+            </a>
+          </li>
+
+          <li ng-show="addTagMode">
+              <input type="text"
+                     class="input-small grafana-target-segment-input"
+                     spellcheck='false'
+                     bs-typeahead="suggestTagKeys"
+                     data-min-length=0 data-items=100
+                     ng-model="target.currentTagKey"
+                     placeholder="key">
+              <input type="text"
+                     class="input-small grafana-target-segment-input"
+                     spellcheck='false'
+                     bs-typeahead="suggestTagValues"
+                     data-min-length=0 data-items=100
+                     ng-model="target.currentTagValue"
+                     placeholder="value">
+              <a ng-click="addTag()">
+                <i class="icon-plus-sign"></i>
+              </a>
+              <a bs-tooltip="target.errors.tags"
+                 style="color: rgb(229, 189, 28)"
+                 ng-show="target.errors.tags">
+                <i class="icon-warning-sign"></i>
+              </a>
+          </li>
+        </ul>
+
+        <!-- <ul class="grafana-segment-list" role="menu">
+          <li>
+            <input type="text"
+                   class="input-xlarge grafana-target-segment-input"
+                   ng-model="target.metric"
+                   spellcheck='false'
+                   bs-typeahead="suggestMetrics"
+                   placeholder="metric name"
+                   data-min-length=0 data-items=100
+                   ng-blur="targetBlur()"
+                   >
+            <a bs-tooltip="target.errors.metric"
+               style="color: rgb(229, 189, 28)"
+               ng-show="target.errors.metric">
+              <i class="icon-warning-sign"></i>
+            </a>
+          </li>
+
+          <li>
+            <select ng-model="target.aggregator"
+                    class="grafana-target-segment-input input-small"
+                    ng-options="agg for agg in aggregators"
+                    ng-change="targetBlur()">
+            </select>
+            <a bs-tooltip="target.errors.aggregator"
+               style="color: rgb(229, 189, 28)"
+               ng-show="target.errors.aggregator">
+              <i class="icon-warning-sign"></i>
+            </a>
+          </li>
+
+          <li class="grafana-target-segment">
+              Rate:
+              <input type="checkbox"
+                     class="grafana-target-option-checkbox"
+                     ng-model="target.shouldComputeRate"
+                     ng-change="targetBlur()"
+                     >
+              <span ng-hide="!target.shouldComputeRate">
+                Counter:
+                <input type="checkbox"
+                       class="grafana-target-option-checkbox"
+                       ng-disabled="!target.shouldComputeRate"
+                       ng-model="target.isCounter"
+                       ng-change="targetBlur()"
+                       >
+              </span>
+          </li>
+
+          <li class="grafana-target-segment">
+            Downsample:
+            <input type="checkbox"
+                   class="grafana-target-option-checkbox"
+                   ng-model="target.shouldDownsample"
+                   ng-change="targetBlur(target)"
+                   >
+            <div ng-hide="!target.shouldDownsample">
+              <table>
+                <tr>
+                  <td>
+                    Interval:
+                  </td>
+                  <td>
+                    <input type="text"
+                           class="input-small"
+                           ng-disabled="!target.shouldDownsample"
+                           ng-model="target.downsampleInterval"
+                           ng-change="targetBlur()">
+                  </td>
+                  <td>
+                    <a bs-tooltip="target.errors.downsampleInterval"
+                       style="color: rgb(229, 189, 28)"
+                       ng-show="target.errors.downsampleInterval">
+                      <i class="icon-warning-sign"></i>
+                    </a>
+                  </td>
+                </tr>
+                <tr>
+                  <td>Aggregator:</td>
+                  <td>
+                    <select ng-model="target.downsampleAggregator"
+                            class="grafana-target-segment-input input-small"
+                            ng-options="agg for agg in aggregators"
+                            ng-change="targetBlur()"
+                            >
+                      <option value="">Pick one</option>
+                    </select>
+                  </td>
+                  <td>
+                    <a bs-tooltip="target.errors.downsampleAggregator"
+                       style="color: rgb(229, 189, 28)"
+                       ng-show="target.errors.downsampleAggregator">
+                      <i class="icon-warning-sign"></i>
+                    </a>
+                  </td>
+                </tr>
+              </table>
+            </div>
+          </li>
+
+          <li class="grafana-target-segment">
+              Tags:
+          </li>
+
+          <li>
+            <div>
+              <input type="text"
+                     class="input-small grafana-target-segment-input"
+                     spellcheck='false'
+                     bs-typeahead="suggestTagKeys"
+                     data-min-length=0 data-items=100
+                     ng-model="target.currentTagKey"
+                     placeholder="key">
+              <input type="text"
+                     class="input-small grafana-target-segment-input"
+                     spellcheck='false'
+                     bs-typeahead="suggestTagValues"
+                     data-min-length=0 data-items=100
+                     ng-model="target.currentTagValue"
+                     placeholder="value">
+              <a ng-click="addTag()">
+                <i class="icon-plus-sign"></i>
+              </a>
+              <a bs-tooltip="target.errors.tags"
+                 style="color: rgb(229, 189, 28)"
+                 ng-show="target.errors.tags">
+                <i class="icon-warning-sign"></i>
+              </a>
+            </div>
+            <table ng-hide="_.isEmpty(target.tags)">
+              <tr>
+                <th>Key</th>
+                <th>Value</td>
+              <tr ng-repeat="(key, value) in target.tags track by $index">
+                <td>{{ key }}</td>
+                <td>{{ value }}</td>
+                <td>
+                  <a ng-click="removeTag(key)">
+                    <i class="icon-remove"></i>
+                  </a>
+                </td>
+              </tr>
+            </table>
+          </li>
+        </ul> -->
+
+        <div class="clearfix"></div>
+      </div>
+    </div>
+  </div>
+</div>

+ 4 - 1
src/app/services/datasourceSrv.js

@@ -4,13 +4,14 @@ define([
   'config',
   './graphite/graphiteDatasource',
   './influxdb/influxdbDatasource',
+  './opentsdb/opentsdbDatasource',
 ],
 function (angular, _, config) {
   'use strict';
 
   var module = angular.module('kibana.services');
 
-  module.service('datasourceSrv', function($q, $http, GraphiteDatasource, InfluxDatasource) {
+  module.service('datasourceSrv', function($q, filterSrv, $http, GraphiteDatasource, InfluxDatasource, OpenTSDBDatasource) {
 
     this.init = function() {
       var defaultDatasource = _.findWhere(_.values(config.datasources), { default: true });
@@ -23,6 +24,8 @@ function (angular, _, config) {
         return new GraphiteDatasource(ds);
       case 'influxdb':
         return new InfluxDatasource(ds);
+      case 'opentsdb':
+        return new OpenTSDBDatasource(ds);
       }
     };
 

+ 155 - 0
src/app/services/opentsdb/opentsdbDatasource.js

@@ -0,0 +1,155 @@
+define([
+  'angular',
+  'underscore',
+  'kbn'
+],
+function (angular, _, kbn) {
+  'use strict';
+
+  var module = angular.module('kibana.services');
+
+  module.factory('OpenTSDBDatasource', function($q, $http) {
+
+    function OpenTSDBDatasource(datasource) {
+      this.type = 'opentsdb';
+      this.editorSrc = 'app/partials/opentsdb/editor.html';
+      this.url = datasource.url;
+      this.name = datasource.name;
+    }
+
+    // Called once per panel (graph)
+    OpenTSDBDatasource.prototype.query = function(filterSrv, options) {
+      var start = convertToTSDBTime(options.range.from);
+      var end = convertToTSDBTime(options.range.to);
+      var queries = _.compact(_.map(options.targets, convertTargetToQuery));
+
+      // No valid targets, return the empty result to save a round trip.
+      if (_.isEmpty(queries)) {
+        var d = $q.defer();
+        d.resolve({ data: [] });
+        return d.promise;
+      }
+
+      var groupByTags = {};
+      _.each(queries, function(query) {
+        _.each(query.tags, function(val, key) {
+          if (val === "*") {
+            groupByTags[key] = true;
+          }
+        });
+      });
+
+      return this.performTimeSeriesQuery(queries, start, end)
+        .then(function(response) {
+          var result = _.map(response.data, function(metricData) {
+            return transformMetricData(metricData, groupByTags);
+          });
+          return { data: result };
+        });
+    };
+
+    OpenTSDBDatasource.prototype.performTimeSeriesQuery = function(queries, start, end) {
+      var reqBody = {
+        start: start,
+        queries: queries
+      };
+
+      // Relative queries (e.g. last hour) don't include an end time
+      if (end) {
+        reqBody.end = end;
+      }
+
+      var options = {
+        method: 'POST',
+        url: this.url + '/api/query',
+        data: reqBody
+      };
+
+      return $http(options);
+    };
+
+    OpenTSDBDatasource.prototype.performSuggestQuery = function(query, type) {
+      var options = {
+        method: 'GET',
+        url: this.url + '/api/suggest',
+        params: {
+          type: type,
+          q: query
+        }
+      };
+      return $http(options).then(function(result) {
+        return result.data;
+      });
+    };
+
+    function transformMetricData(md, groupByTags) {
+      var dps = [];
+
+      // TSDB returns datapoints has a hash of ts => value.
+      // Can't use _.pairs(invert()) because it stringifies keys/values
+      _.each(md.dps, function (v, k) {
+        dps.push([v, k]);
+      });
+
+      var target = md.metric;
+      if (!_.isEmpty(md.tags)) {
+        var tagData = [];
+
+        _.each(_.pairs(md.tags), function(tag) {
+          if (_.has(groupByTags, tag[0])) {
+            tagData.push(tag[0] + "=" + tag[1]);
+          }
+        });
+
+        if (!_.isEmpty(tagData)) {
+          target = target + "{" + tagData.join(", ") + "}";
+        }
+      }
+
+      return { target: target, datapoints: dps };
+    }
+
+    function convertTargetToQuery(target) {
+      if (!target.metric) {
+        return null;
+      }
+
+      var query = {
+        metric: target.metric,
+        aggregator: "avg"
+      };
+
+      if (target.aggregator) {
+        query.aggregator = target.aggregator;
+      }
+
+      if (target.shouldComputeRate) {
+        query.rate = true;
+        query.rateOptions = {
+          counter: !!target.isCounter
+        };
+      }
+
+      if (target.shouldDownsample) {
+        query.downsample = target.downsampleInterval + "-" + target.downsampleAggregator;
+      }
+
+      query.tags = angular.copy(target.tags);
+
+      return query;
+    }
+
+    function convertToTSDBTime(date) {
+      if (date === 'now') {
+        return null;
+      }
+
+      date = kbn.parseDate(date);
+
+      return date.getTime();
+    }
+
+    return OpenTSDBDatasource;
+  });
+
+});

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
src/css/bootstrap.dark.min.css


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
src/css/bootstrap.light.min.css


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
src/css/default.min.css


+ 4 - 0
src/css/less/grafana.less

@@ -371,6 +371,10 @@ input[type=text].grafana-target-segment-input {
   padding: 8px 4px;
 }
 
+input[type=checkbox].grafana-target-option-checkbox {
+  margin: 0;
+}
+
 select.grafana-target-segment-input {
   border: none;
   border-right: 1px solid @grafanaTargetSegmentBorder;

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio