瀏覽代碼

Add prometheus datasource

Jimmi Dyson 10 年之前
父節點
當前提交
bf98cfeadc

+ 256 - 0
public/app/plugins/datasource/prometheus/datasource.js

@@ -0,0 +1,256 @@
+define([
+  'angular',
+  'lodash',
+  'kbn',
+  'moment',
+  'app/core/utils/datemath',
+  './directives',
+  './queryCtrl',
+],
+function (angular, _, kbn, dateMath) {
+  'use strict';
+
+  var module = angular.module('grafana.services');
+
+  module.factory('PrometheusDatasource', function($q, backendSrv, templateSrv) {
+
+    function PrometheusDatasource(datasource) {
+      this.type = 'prometheus';
+      this.editorSrc = 'app/features/prometheus/partials/query.editor.html';
+      this.name = datasource.name;
+      this.supportMetrics = true;
+
+      var url = datasource.url;
+      if (url[url.length-1] === '/') {
+        // remove trailing slash
+        url = url.substr(0, url.length - 1);
+      }
+      this.url = url;
+      this.basicAuth = datasource.basicAuth;
+      this.lastErrors = {};
+    }
+
+    PrometheusDatasource.prototype._request = function(method, url) {
+      var options = {
+        url: this.url + url,
+        method: method
+      };
+
+      if (this.basicAuth) {
+        options.withCredentials = true;
+        options.headers = {
+          "Authorization": this.basicAuth
+        };
+      }
+
+      return backendSrv.datasourceRequest(options);
+    };
+
+    // Called once per panel (graph)
+    PrometheusDatasource.prototype.query = function(options) {
+      var start = getPrometheusTime(options.range.from, false);
+      var end = getPrometheusTime(options.range.to, true);
+
+      var queries = [];
+      _.each(options.targets, _.bind(function(target) {
+        if (!target.expr || target.hide) {
+          return;
+        }
+
+        var query = {};
+        query.expr = templateSrv.replace(target.expr, options.scopedVars);
+
+        var interval = target.interval || options.interval;
+        var intervalFactor = target.intervalFactor || 1;
+        query.step = this.calculateInterval(interval, intervalFactor);
+
+        queries.push(query);
+      }, this));
+
+      // 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 allQueryPromise = _.map(queries, _.bind(function(query) {
+        return this.performTimeSeriesQuery(query, start, end);
+      }, this));
+
+      var self = this;
+      return $q.all(allQueryPromise)
+        .then(function(allResponse) {
+          var result = [];
+
+          _.each(allResponse, function(response, index) {
+            if (response.status === 'error') {
+              self.lastErrors.query = response.error;
+              throw response.error;
+            }
+            delete self.lastErrors.query;
+
+            _.each(response.data.data.result, function(metricData) {
+              result.push(transformMetricData(metricData, options.targets[index]));
+            });
+          });
+
+          return { data: result };
+        });
+    };
+
+    PrometheusDatasource.prototype.performTimeSeriesQuery = function(query, start, end) {
+      var url = '/api/v1/query_range?query=' + encodeURIComponent(query.expr) + '&start=' + start + '&end=' + end;
+
+      var step = query.step;
+      var range = Math.floor(end - start);
+      // Prometheus drop query if range/step > 11000
+      // calibrate step if it is too big
+      if (step !== 0 && range / step > 11000) {
+        step = Math.floor(range / 11000);
+      }
+      url += '&step=' + step;
+
+      return this._request('GET', url);
+    };
+
+    PrometheusDatasource.prototype.performSuggestQuery = function(query) {
+      var url = '/api/v1/label/__name__/values';
+
+      return this._request('GET', url).then(function(result) {
+        var suggestData = _.filter(result.data.data, function(metricName) {
+          return metricName.indexOf(query) !==  1;
+        });
+
+        return suggestData;
+      });
+    };
+
+    PrometheusDatasource.prototype.metricFindQuery = function(query) {
+      var url;
+
+      var metricsQuery = query.match(/^[a-zA-Z_:*][a-zA-Z0-9_:*]*/);
+      var labelValuesQuery = query.match(/^label_values\((.+)\)/);
+
+      if (labelValuesQuery) {
+        // return label values
+        url = '/api/v1/label/' + labelValuesQuery[1] + '/values';
+
+        return this._request('GET', url).then(function(result) {
+          return _.map(result.data.data, function(value) {
+            return {text: value};
+          });
+        });
+      } else if (metricsQuery != null && metricsQuery[0].indexOf('*') >= 0) {
+        // if query has wildcard character, return metric name list
+        url = '/api/v1/label/__name__/values';
+
+        return this._request('GET', url)
+          .then(function(result) {
+            return _.chain(result.data.data)
+              .filter(function(metricName) {
+                var r = new RegExp(metricsQuery[0].replace(/\*/g, '.*'));
+                return r.test(metricName);
+              })
+              .map(function(matchedMetricName) {
+                return {
+                  text: matchedMetricName,
+                  expandable: true
+                };
+              })
+              .value();
+          });
+      } else {
+        // if query contains full metric name, return metric name and label list
+        url = '/api/v1/query?query=' + encodeURIComponent(query);
+
+        return this._request('GET', url)
+          .then(function(result) {
+            return _.map(result.data.result, function(metricData) {
+              return {
+                text: getOriginalMetricName(metricData.metric),
+                expandable: true
+              };
+            });
+          });
+      }
+    };
+
+    PrometheusDatasource.prototype.testDatasource = function() {
+      return this.metricFindQuery('*').then(function() {
+        return { status: 'success', message: 'Data source is working', title: 'Success' };
+      });
+    };
+
+    PrometheusDatasource.prototype.calculateInterval = function(interval, intervalFactor) {
+      var sec = kbn.interval_to_seconds(interval);
+
+      if (sec < 1) {
+        sec = 1;
+      }
+
+      return sec * intervalFactor;
+    };
+
+    function transformMetricData(md, options) {
+      var dps = [],
+          metricLabel = null;
+
+      metricLabel = createMetricLabel(md.metric, options);
+
+      dps = _.map(md.values, function(value) {
+        return [parseFloat(value[1]), value[0] * 1000];
+      });
+
+      return { target: metricLabel, datapoints: dps };
+    }
+
+    function createMetricLabel(labelData, options) {
+      if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) {
+        return getOriginalMetricName(labelData);
+      }
+
+      var originalSettings = _.templateSettings;
+      _.templateSettings = {
+        interpolate: /\{\{(.+?)\}\}/g
+      };
+
+      var template = _.template(templateSrv.replace(options.legendFormat));
+      var metricName;
+      try {
+        metricName = template(labelData);
+      } catch (e) {
+        metricName = '{}';
+      }
+
+      _.templateSettings = originalSettings;
+
+      return metricName;
+    }
+
+    function getOriginalMetricName(labelData) {
+      var metricName = labelData.__name__ || '';
+      delete labelData.__name__;
+      var labelPart = _.map(_.pairs(labelData), function(label) {
+        return label[0] + '="' + label[1] + '"';
+      }).join(',');
+      return metricName + '{' + labelPart + '}';
+    }
+
+    function getPrometheusTime(date, roundUp) {
+      if (_.isString(date)) {
+        if (date === 'now') {
+          return 'now()';
+        }
+        if (date.indexOf('now-') >= 0 && date.indexOf('/') === -1) {
+          return date.replace('now', 'now()').replace('-', ' - ');
+        }
+        date = dateMath.parse(date, roundUp);
+      }
+      return (date.valueOf() / 1000).toFixed(0);
+    }
+
+    return PrometheusDatasource;
+  });
+
+});

+ 13 - 0
public/app/plugins/datasource/prometheus/directives.js

@@ -0,0 +1,13 @@
+define([
+  'angular',
+],
+function (angular) {
+  'use strict';
+
+  var module = angular.module('grafana.directives');
+
+  module.directive('metricQueryEditorPrometheus', function() {
+    return {controller: 'PrometheusQueryCtrl', templateUrl: 'app/plugins/datasource/prometheus/partials/query.editor.html'};
+  });
+
+});

+ 4 - 0
public/app/plugins/datasource/prometheus/partials/config.html

@@ -0,0 +1,4 @@
+<div ng-include="httpConfigPartialSrc"></div>
+
+<br>
+

+ 143 - 0
public/app/plugins/datasource/prometheus/partials/query.editor.html

@@ -0,0 +1,143 @@
+<div class="tight-form">
+  <ul class="tight-form-list pull-right">
+    <li class="tight-form-item small" ng-show="target.datasource">
+      <em>{{target.datasource}}</em>
+    </li>
+    <li class="tight-form-item">
+      <div class="dropdown">
+        <a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
+          <i class="fa fa-bars"></i>
+        </a>
+        <ul class="dropdown-menu pull-right" role="menu">
+          <li role="menuitem"><a tabindex="1" ng-click="toggleQueryMode()">Switch editor mode</a></li>
+          <li role="menuitem"><a tabindex="1" ng-click="duplicateDataQuery(target)">Duplicate</a></li>
+          <li role="menuitem"><a tabindex="1" ng-click="moveDataQuery($index, $index-1)">Move up</a></li>
+          <li role="menuitem"><a tabindex="1" ng-click="moveDataQuery($index, $index+1)">Move down</a></li>
+        </ul>
+      </div>
+    </li>
+    <li class="tight-form-item last">
+      <a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
+        <i class="fa fa-remove"></i>
+      </a>
+    </li>
+  </ul>
+
+  <ul class="tight-form-list">
+    <li class="tight-form-item" style="min-width: 15px; text-align: center">
+      {{target.refId}}
+    </li>
+    <li>
+      <a  class="tight-form-item"
+          ng-click="target.hide = !target.hide; get_data();"
+          role="menuitem">
+        <i class="fa fa-eye"></i>
+      </a>
+    </li>
+  </ul>
+
+  <ul class="tight-form-list" role="menu">
+    <li class="tight-form-item" style="width: 94px">
+      Query
+    </li>
+    <li>
+      <input type="text"
+             class="input-xxlarge tight-form-input"
+             ng-model="target.expr"
+             spellcheck='false'
+             placeholder="query expression"
+             data-min-length=0 data-items=100
+             ng-model-onblur
+             ng-change="refreshMetricData()"
+             >
+      <a bs-tooltip="target.datasourceErrors.query"
+         style="color: rgb(229, 189, 28)"
+         ng-show="target.datasourceErrors.query">
+        <i class="fa fa-warning"></i>
+      </a>
+    </li>
+    <li class="tight-form-item">
+      Metric
+    </li>
+    <li>
+      <input type="text"
+             class="input-medium tight-form-input"
+             ng-model="target.metric"
+             spellcheck='false'
+             bs-typeahead="suggestMetrics"
+             placeholder="metric name"
+             data-min-length=0 data-items=100
+             >
+      <a bs-tooltip="target.errors.metric"
+         style="color: rgb(229, 189, 28)"
+         ng-show="target.errors.metric">
+        <i class="fa fa-warning"></i>
+      </a>
+    </li>
+
+  </ul>
+
+  <div class="clearfix"></div>
+</div>
+
+<div class="tight-form">
+  <ul class="tight-form-list" role="menu">
+    <li class="tight-form-item tight-form-align" style="width: 94px">
+      Legend format
+    </li>
+    <li>
+      <input type="text"
+             class="tight-form-input input-xxlarge"
+             ng-model="target.legendFormat"
+             spellcheck='false'
+             placeholder="legend format"
+             data-min-length=0 data-items=1000
+             ng-model-onblur
+             ng-change="refreshMetricData()"
+             />
+    </li>
+  </ul>
+
+  <div class="clearfix"></div>
+</div>
+
+<div class="tight-form">
+  <ul class="tight-form-list" role="menu">
+    <li class="tight-form-item tight-form-align" style="width: 94px">
+      Step
+    </li>
+    <li>
+      <input type="text"
+             class="input-mini tight-form-input"
+             ng-model="target.interval"
+             bs-tooltip="'Leave blank for auto handling based on time range and panel width'"
+             data-placement="right"
+             spellcheck='false'
+             placeholder="{{target.calculatedInterval}}"
+             data-min-length=0 data-items=100
+             ng-model-onblur
+             ng-change="refreshMetricData()"
+             />
+    </li>
+
+    <li class="tight-form-item">
+      Resolution
+    </li>
+    <li>
+      <select ng-model="target.intervalFactor"
+             class="tight-form-input input-mini"
+             ng-options="r.factor as r.label for r in resolutions"
+             ng-change="refreshMetricData()">
+      </select>
+    </li>
+
+    <li class="tight-form-item">
+      <a href="{{target.prometheusLink}}" target="_blank" bs-tooltip="'Link to Graph in Prometheus'">
+        <i class="fa fa-share-square-o"></i>
+      </a>
+    </li>
+
+  </ul>
+
+  <div class="clearfix"></div>
+</div>

+ 15 - 0
public/app/plugins/datasource/prometheus/plugin.json

@@ -0,0 +1,15 @@
+{
+  "pluginType": "datasource",
+  "name": "Prometheus",
+
+  "type": "prometheus",
+  "serviceName": "PrometheusDatasource",
+
+  "module": "app/plugins/datasource/prometheus/datasource",
+
+  "partials": {
+    "config": "app/plugins/datasource/prometheus/partials/config.html"
+  },
+
+  "metrics": true
+}

+ 133 - 0
public/app/plugins/datasource/prometheus/queryCtrl.js

@@ -0,0 +1,133 @@
+define([
+  'angular',
+  'lodash',
+  'kbn',
+  'app/core/utils/datemath',
+],
+function (angular, _, kbn, dateMath) {
+  'use strict';
+
+  var module = angular.module('grafana.controllers');
+
+  module.controller('PrometheusQueryCtrl', function($scope) {
+
+    $scope.init = function() {
+      $scope.target.errors = validateTarget();
+      $scope.target.datasourceErrors = {};
+
+      if (!$scope.target.expr) {
+        $scope.target.expr = '';
+      }
+      $scope.target.metric = '';
+
+      $scope.resolutions = [
+        { factor:  1, },
+        { factor:  2, },
+        { factor:  3, },
+        { factor:  5, },
+        { factor: 10, },
+      ];
+      $scope.resolutions = _.map($scope.resolutions, function(r) {
+        r.label = '1/' + r.factor;
+        return r;
+      });
+      if (!$scope.target.intervalFactor) {
+        $scope.target.intervalFactor = 2; // default resolution is 1/2
+      }
+
+      $scope.calculateInterval();
+      $scope.$on('render', function() {
+        $scope.calculateInterval(); // re-calculate interval when time range is updated
+      });
+      $scope.target.prometheusLink = $scope.linkToPrometheus();
+
+      $scope.$on('typeahead-updated', function() {
+        $scope.$apply($scope.inputMetric);
+        $scope.refreshMetricData();
+      });
+
+      $scope.datasource.lastErrors = {};
+      $scope.$watch('datasource.lastErrors', function() {
+        $scope.target.datasourceErrors = $scope.datasource.lastErrors;
+      }, true);
+    };
+
+    $scope.refreshMetricData = function() {
+      $scope.target.errors = validateTarget($scope.target);
+      $scope.calculateInterval();
+      $scope.target.prometheusLink = $scope.linkToPrometheus();
+
+      // 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.inputMetric = function() {
+      $scope.target.expr += $scope.target.metric;
+      $scope.target.metric = '';
+    };
+
+    $scope.moveMetricQuery = function(fromIndex, toIndex) {
+      _.move($scope.panel.targets, fromIndex, toIndex);
+    };
+
+    $scope.suggestMetrics = function(query, callback) {
+      $scope.datasource
+        .performSuggestQuery(query)
+        .then(callback);
+    };
+
+    $scope.linkToPrometheus = function() {
+      var from = dateMath.parse($scope.dashboard.time.from, false);
+      var to = dateMath.parse($scope.dashboard.time.to, true);
+
+      if ($scope.panel.timeFrom) {
+        from = dateMath.parseDateMath('-' + $scope.panel.timeFrom, to, false);
+      }
+      if ($scope.panel.timeShift) {
+        from = dateMath.parseDateMath('-' + $scope.panel.timeShift, from, false);
+        to = dateMath.parseDateMath('-' + $scope.panel.timeShift, to, true);
+      }
+
+      var range = Math.ceil((to.valueOf()- from.valueOf()) / 1000);
+
+      var endTime = to.format('YYYY-MM-DD HH:MM');
+
+      var step = kbn.interval_to_seconds(this.target.calculatedInterval);
+      if (step !== 0 && range / step > 11000) {
+        step = Math.floor(range / 11000);
+      }
+
+      var expr = {
+        expr: $scope.target.expr,
+        range_input: range + 's',
+        end_input: endTime,
+        //step_input: step,
+        step_input: '',
+        stacked: $scope.panel.stack,
+        tab: 0
+      };
+
+      var hash = encodeURIComponent(JSON.stringify([expr]));
+      return $scope.datasource.url + '/graph#' + hash;
+    };
+
+    $scope.calculateInterval = function() {
+      var interval = $scope.target.interval || $scope.interval;
+      var calculatedInterval = $scope.datasource.calculateInterval(interval, $scope.target.intervalFactor);
+      $scope.target.calculatedInterval = kbn.secondsToHms(calculatedInterval);
+    };
+
+    // TODO: validate target
+    function validateTarget() {
+      var errs = {};
+
+      return errs;
+    }
+
+    $scope.init();
+  });
+
+});

+ 61 - 0
public/test/specs/prometheus-datasource-specs.js

@@ -0,0 +1,61 @@
+define([
+  './helpers',
+  'moment',
+  'app/plugins/datasource/prometheus/datasource',
+  'app/services/backendSrv',
+  'app/services/alertSrv'
+], function(helpers, moment) {
+  'use strict';
+
+  describe('PrometheusDatasource', function() {
+    var ctx = new helpers.ServiceTestContext();
+
+    beforeEach(module('grafana.services'));
+    beforeEach(ctx.providePhase(['templateSrv']));
+    beforeEach(ctx.createService('PrometheusDatasource'));
+    beforeEach(function() {
+      ctx.ds = new ctx.service({ url: '', user: 'test', password: 'mupp' });
+    });
+
+    describe('When querying prometheus with one target using query editor target spec', function() {
+      var results;
+      var urlExpected = '/api/v1/query_range?query=' +
+                        encodeURIComponent('test{job="testjob"}') +
+                        '&start=1443438675&end=1443460275&step=60';
+      var query = {
+        range: { from: moment(1443438674760), to: moment(1443460274760) },
+        targets: [{ expr: 'test{job="testjob"}' }],
+        interval: '60s'
+      };
+
+      var response = {
+        "status":"success",
+        "data":{
+          "resultType":"matrix",
+          "result":[{
+            "metric":{"__name__":"test", "job":"testjob"},
+            "values":[[1443454528,"3846"]]
+          }]
+        }
+      };
+
+      beforeEach(function() {
+        ctx.$httpBackend.expect('GET', urlExpected).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 return series list', function() {
+        expect(results.data.length).to.be(1);
+        expect(results.data[0].target).to.be('test{job="testjob"}');
+      });
+
+    });
+
+  });
+});
+

+ 1 - 0
tasks/options/requirejs.js

@@ -63,6 +63,7 @@ module.exports = function(config,grunt) {
           'app/plugins/datasource/grafana/datasource',
           'app/plugins/datasource/graphite/datasource',
           'app/plugins/datasource/influxdb/datasource',
+          'app/plugins/datasource/prometheus/datasource',
         ]
       },
     ];