mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 19:00:54 -06:00
Add prometheus datasource
This commit is contained in:
parent
cb7424ce5e
commit
bf98cfeadc
256
public/app/plugins/datasource/prometheus/datasource.js
Normal file
256
public/app/plugins/datasource/prometheus/datasource.js
Normal file
@ -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
public/app/plugins/datasource/prometheus/directives.js
Normal file
13
public/app/plugins/datasource/prometheus/directives.js
Normal file
@ -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'};
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,4 @@
|
||||
<div ng-include="httpConfigPartialSrc"></div>
|
||||
|
||||
<br>
|
||||
|
@ -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
public/app/plugins/datasource/prometheus/plugin.json
Normal file
15
public/app/plugins/datasource/prometheus/plugin.json
Normal file
@ -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
public/app/plugins/datasource/prometheus/queryCtrl.js
Normal file
133
public/app/plugins/datasource/prometheus/queryCtrl.js
Normal file
@ -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
public/test/specs/prometheus-datasource-specs.js
Normal file
61
public/test/specs/prometheus-datasource-specs.js
Normal file
@ -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"}');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -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',
|
||||
]
|
||||
},
|
||||
];
|
||||
|
Loading…
Reference in New Issue
Block a user