Merge branch 'mysql-connector'
This commit is contained in:
74
dist/datasource-zabbix/config.controller.js
vendored
Normal file
74
dist/datasource-zabbix/config.controller.js
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
'use strict';
|
||||
|
||||
System.register(['lodash'], function (_export, _context) {
|
||||
"use strict";
|
||||
|
||||
var _, _createClass, SUPPORTED_SQL_DS, defaultConfig, ZabbixDSConfigController;
|
||||
|
||||
function _classCallCheck(instance, Constructor) {
|
||||
if (!(instance instanceof Constructor)) {
|
||||
throw new TypeError("Cannot call a class as a function");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
setters: [function (_lodash) {
|
||||
_ = _lodash.default;
|
||||
}],
|
||||
execute: function () {
|
||||
_createClass = function () {
|
||||
function defineProperties(target, props) {
|
||||
for (var i = 0; i < props.length; i++) {
|
||||
var descriptor = props[i];
|
||||
descriptor.enumerable = descriptor.enumerable || false;
|
||||
descriptor.configurable = true;
|
||||
if ("value" in descriptor) descriptor.writable = true;
|
||||
Object.defineProperty(target, descriptor.key, descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
return function (Constructor, protoProps, staticProps) {
|
||||
if (protoProps) defineProperties(Constructor.prototype, protoProps);
|
||||
if (staticProps) defineProperties(Constructor, staticProps);
|
||||
return Constructor;
|
||||
};
|
||||
}();
|
||||
|
||||
SUPPORTED_SQL_DS = ['mysql'];
|
||||
defaultConfig = {
|
||||
dbConnection: {
|
||||
enable: false
|
||||
}
|
||||
};
|
||||
|
||||
_export('ZabbixDSConfigController', ZabbixDSConfigController = function () {
|
||||
/** @ngInject */
|
||||
function ZabbixDSConfigController($scope, $injector, datasourceSrv) {
|
||||
_classCallCheck(this, ZabbixDSConfigController);
|
||||
|
||||
this.datasourceSrv = datasourceSrv;
|
||||
|
||||
_.defaults(this.current.jsonData, defaultConfig);
|
||||
this.sqlDataSources = this.getSupportedSQLDataSources();
|
||||
}
|
||||
|
||||
_createClass(ZabbixDSConfigController, [{
|
||||
key: 'getSupportedSQLDataSources',
|
||||
value: function getSupportedSQLDataSources() {
|
||||
var datasources = this.datasourceSrv.getAll();
|
||||
return _.filter(datasources, function (ds) {
|
||||
return _.includes(SUPPORTED_SQL_DS, ds.type);
|
||||
});
|
||||
}
|
||||
}]);
|
||||
|
||||
return ZabbixDSConfigController;
|
||||
}());
|
||||
|
||||
_export('ZabbixDSConfigController', ZabbixDSConfigController);
|
||||
|
||||
ZabbixDSConfigController.templateUrl = 'datasource-zabbix/partials/config.html';
|
||||
}
|
||||
};
|
||||
});
|
||||
//# sourceMappingURL=config.controller.js.map
|
||||
1
dist/datasource-zabbix/config.controller.js.map
vendored
Normal file
1
dist/datasource-zabbix/config.controller.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../src/datasource-zabbix/config.controller.js"],"names":["_","SUPPORTED_SQL_DS","defaultConfig","dbConnection","enable","ZabbixDSConfigController","$scope","$injector","datasourceSrv","defaults","current","jsonData","sqlDataSources","getSupportedSQLDataSources","datasources","getAll","filter","includes","ds","type","templateUrl"],"mappings":";;;;;;;;;;;;;;;AAAOA,O;;;;;;;;;;;;;;;;;;;;;AAEDC,sB,GAAmB,CAAC,OAAD,C;AAEnBC,mB,GAAgB;AACpBC,sBAAc;AACZC,kBAAQ;AADI;AADM,O;;0CAMTC,wB;AACX;AACA,0CAAYC,MAAZ,EAAoBC,SAApB,EAA+BC,aAA/B,EAA8C;AAAA;;AAC5C,eAAKA,aAAL,GAAqBA,aAArB;;AAEAR,YAAES,QAAF,CAAW,KAAKC,OAAL,CAAaC,QAAxB,EAAkCT,aAAlC;AACA,eAAKU,cAAL,GAAsB,KAAKC,0BAAL,EAAtB;AACD;;;;uDAE4B;AAC3B,gBAAIC,cAAc,KAAKN,aAAL,CAAmBO,MAAnB,EAAlB;AACA,mBAAOf,EAAEgB,MAAF,CAASF,WAAT,EAAsB,cAAM;AACjC,qBAAOd,EAAEiB,QAAF,CAAWhB,gBAAX,EAA6BiB,GAAGC,IAAhC,CAAP;AACD,aAFM,CAAP;AAGD;;;;;;;;AAGHd,+BAAyBe,WAAzB,GAAuC,wCAAvC","file":"config.controller.js","sourcesContent":["import _ from 'lodash';\n\nconst SUPPORTED_SQL_DS = ['mysql'];\n\nconst defaultConfig = {\n dbConnection: {\n enable: false,\n }\n};\n\nexport class ZabbixDSConfigController {\n /** @ngInject */\n constructor($scope, $injector, datasourceSrv) {\n this.datasourceSrv = datasourceSrv;\n\n _.defaults(this.current.jsonData, defaultConfig);\n this.sqlDataSources = this.getSupportedSQLDataSources();\n }\n\n getSupportedSQLDataSources() {\n let datasources = this.datasourceSrv.getAll();\n return _.filter(datasources, ds => {\n return _.includes(SUPPORTED_SQL_DS, ds.type);\n });\n }\n}\n\nZabbixDSConfigController.templateUrl = 'datasource-zabbix/partials/config.html';\n"]}
|
||||
49
dist/datasource-zabbix/datasource.js
vendored
49
dist/datasource-zabbix/datasource.js
vendored
@@ -23,10 +23,23 @@ System.register(['lodash', 'app/core/utils/datemath', './utils', './migrations',
|
||||
});
|
||||
}
|
||||
|
||||
function getConsolidateBy(target) {
|
||||
var consolidateBy = 'avg';
|
||||
var funcDef = _.find(target.functions, function (func) {
|
||||
return func.def.name === 'consolidateBy';
|
||||
});
|
||||
if (funcDef && funcDef.params && funcDef.params.length) {
|
||||
consolidateBy = funcDef.params[0];
|
||||
}
|
||||
return consolidateBy;
|
||||
}
|
||||
|
||||
function downsampleSeries(timeseries_data, options) {
|
||||
var defaultAgg = dataProcessor.aggregationFunctions['avg'];
|
||||
var consolidateByFunc = dataProcessor.aggregationFunctions[options.consolidateBy] || defaultAgg;
|
||||
return _.map(timeseries_data, function (timeseries) {
|
||||
if (timeseries.datapoints.length > options.maxDataPoints) {
|
||||
timeseries.datapoints = dataProcessor.groupBy(options.interval, dataProcessor.AVERAGE, timeseries.datapoints);
|
||||
timeseries.datapoints = dataProcessor.groupBy(options.interval, consolidateByFunc, timeseries.datapoints);
|
||||
}
|
||||
return timeseries;
|
||||
});
|
||||
@@ -191,6 +204,9 @@ System.register(['lodash', 'app/core/utils/datemath', './utils', './migrations',
|
||||
this.dashboardSrv = dashboardSrv;
|
||||
this.zabbixAlertingSrv = zabbixAlertingSrv;
|
||||
|
||||
// Use custom format for template variables
|
||||
this.replaceTemplateVars = _.partial(replaceTemplateVars, this.templateSrv);
|
||||
|
||||
// General data source settings
|
||||
this.name = instanceSettings.name;
|
||||
this.url = instanceSettings.url;
|
||||
@@ -215,10 +231,21 @@ System.register(['lodash', 'app/core/utils/datemath', './utils', './migrations',
|
||||
this.addThresholds = instanceSettings.jsonData.addThresholds;
|
||||
this.alertingMinSeverity = instanceSettings.jsonData.alertingMinSeverity || c.SEV_WARNING;
|
||||
|
||||
this.zabbix = new Zabbix(this.url, this.username, this.password, this.basicAuth, this.withCredentials, this.cacheTTL);
|
||||
// Direct DB Connection options
|
||||
this.enableDirectDBConnection = instanceSettings.jsonData.dbConnection.enable;
|
||||
this.sqlDatasourceId = instanceSettings.jsonData.dbConnection.datasourceId;
|
||||
|
||||
// Use custom format for template variables
|
||||
this.replaceTemplateVars = _.partial(replaceTemplateVars, this.templateSrv);
|
||||
var zabbixOptions = {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
basicAuth: this.basicAuth,
|
||||
withCredentials: this.withCredentials,
|
||||
cacheTTL: this.cacheTTL,
|
||||
enableDirectDBConnection: this.enableDirectDBConnection,
|
||||
sqlDatasourceId: this.sqlDatasourceId
|
||||
};
|
||||
|
||||
this.zabbix = new Zabbix(this.url, zabbixOptions);
|
||||
}
|
||||
|
||||
////////////////////////
|
||||
@@ -324,8 +351,14 @@ System.register(['lodash', 'app/core/utils/datemath', './utils', './migrations',
|
||||
};
|
||||
return this.zabbix.getItemsFromTarget(target, getItemOptions).then(function (items) {
|
||||
var getHistoryPromise = void 0;
|
||||
options.consolidateBy = getConsolidateBy(target);
|
||||
|
||||
if (useTrends) {
|
||||
if (_this2.enableDirectDBConnection) {
|
||||
getHistoryPromise = _this2.zabbix.getTrendsDB(items, timeFrom, timeTo, options).then(function (history) {
|
||||
return _this2.zabbix.dbConnector.handleGrafanaTSResponse(history, items);
|
||||
});
|
||||
} else {
|
||||
var valueType = _this2.getTrendValueType(target);
|
||||
getHistoryPromise = _this2.zabbix.getTrend(items, timeFrom, timeTo).then(function (history) {
|
||||
return responseHandler.handleTrends(history, items, valueType);
|
||||
@@ -336,15 +369,21 @@ System.register(['lodash', 'app/core/utils/datemath', './utils', './migrations',
|
||||
return point[c.DATAPOINT_TS];
|
||||
});
|
||||
});
|
||||
|
||||
return timeseries;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Use history
|
||||
if (_this2.enableDirectDBConnection) {
|
||||
getHistoryPromise = _this2.zabbix.getHistoryDB(items, timeFrom, timeTo, options).then(function (history) {
|
||||
return _this2.zabbix.dbConnector.handleGrafanaTSResponse(history, items);
|
||||
});
|
||||
} else {
|
||||
getHistoryPromise = _this2.zabbix.getHistory(items, timeFrom, timeTo).then(function (history) {
|
||||
return responseHandler.handleHistory(history, items);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return getHistoryPromise;
|
||||
}).then(function (timeseries) {
|
||||
|
||||
2
dist/datasource-zabbix/datasource.js.map
vendored
2
dist/datasource-zabbix/datasource.js.map
vendored
File diff suppressed because one or more lines are too long
11
dist/datasource-zabbix/metricFunctions.js
vendored
11
dist/datasource-zabbix/metricFunctions.js
vendored
@@ -80,7 +80,8 @@ System.register(['lodash', 'jquery'], function (_export, _context) {
|
||||
Filter: [],
|
||||
Trends: [],
|
||||
Time: [],
|
||||
Alias: []
|
||||
Alias: [],
|
||||
Special: []
|
||||
};
|
||||
addFuncDef({
|
||||
name: 'groupBy',
|
||||
@@ -225,6 +226,14 @@ System.register(['lodash', 'jquery'], function (_export, _context) {
|
||||
defaultParams: ['/(.*)/', '$1']
|
||||
});
|
||||
|
||||
// Special
|
||||
addFuncDef({
|
||||
name: 'consolidateBy',
|
||||
category: 'Special',
|
||||
params: [{ name: 'type', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count'] }],
|
||||
defaultParams: ['avg']
|
||||
});
|
||||
|
||||
_.each(categories, function (funcList, catName) {
|
||||
categories[catName] = _.sortBy(funcList, 'name');
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
14
dist/datasource-zabbix/module.js
vendored
14
dist/datasource-zabbix/module.js
vendored
@@ -1,9 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
System.register(['./datasource', './query.controller'], function (_export, _context) {
|
||||
System.register(['./datasource', './query.controller', './config.controller'], function (_export, _context) {
|
||||
"use strict";
|
||||
|
||||
var ZabbixAPIDatasource, ZabbixQueryController, ZabbixConfigController, ZabbixQueryOptionsController, ZabbixAnnotationsQueryController;
|
||||
var ZabbixAPIDatasource, ZabbixQueryController, ZabbixDSConfigController, ZabbixQueryOptionsController, ZabbixAnnotationsQueryController;
|
||||
|
||||
function _classCallCheck(instance, Constructor) {
|
||||
if (!(instance instanceof Constructor)) {
|
||||
@@ -16,14 +16,10 @@ System.register(['./datasource', './query.controller'], function (_export, _cont
|
||||
ZabbixAPIDatasource = _datasource.ZabbixAPIDatasource;
|
||||
}, function (_queryController) {
|
||||
ZabbixQueryController = _queryController.ZabbixQueryController;
|
||||
}, function (_configController) {
|
||||
ZabbixDSConfigController = _configController.ZabbixDSConfigController;
|
||||
}],
|
||||
execute: function () {
|
||||
_export('ConfigCtrl', ZabbixConfigController = function ZabbixConfigController() {
|
||||
_classCallCheck(this, ZabbixConfigController);
|
||||
});
|
||||
|
||||
ZabbixConfigController.templateUrl = 'datasource-zabbix/partials/config.html';
|
||||
|
||||
_export('QueryOptionsCtrl', ZabbixQueryOptionsController = function ZabbixQueryOptionsController() {
|
||||
_classCallCheck(this, ZabbixQueryOptionsController);
|
||||
});
|
||||
@@ -38,7 +34,7 @@ System.register(['./datasource', './query.controller'], function (_export, _cont
|
||||
|
||||
_export('Datasource', ZabbixAPIDatasource);
|
||||
|
||||
_export('ConfigCtrl', ZabbixConfigController);
|
||||
_export('ConfigCtrl', ZabbixDSConfigController);
|
||||
|
||||
_export('QueryCtrl', ZabbixQueryController);
|
||||
|
||||
|
||||
2
dist/datasource-zabbix/module.js.map
vendored
2
dist/datasource-zabbix/module.js.map
vendored
@@ -1 +1 @@
|
||||
{"version":3,"sources":["../../src/datasource-zabbix/module.js"],"names":["ZabbixAPIDatasource","ZabbixQueryController","ZabbixConfigController","templateUrl","ZabbixQueryOptionsController","ZabbixAnnotationsQueryController"],"mappings":";;;;;;;;;;;;;;;AAAQA,yB,eAAAA,mB;;AACAC,2B,oBAAAA,qB;;;4BAEFC,sB;;;;AACNA,6BAAuBC,WAAvB,GAAqC,wCAArC;;kCAEMC,4B;;;;AACNA,mCAA6BD,WAA7B,GAA2C,+CAA3C;;sCAEME,gC;;;;AACNA,uCAAiCF,WAAjC,GAA+C,oDAA/C;;4BAGEH,mB;;4BACAE,sB;;2BACAD,qB;;kCACAG,4B;;sCACAC,gC","file":"module.js","sourcesContent":["import {ZabbixAPIDatasource} from './datasource';\nimport {ZabbixQueryController} from './query.controller';\n\nclass ZabbixConfigController {}\nZabbixConfigController.templateUrl = 'datasource-zabbix/partials/config.html';\n\nclass ZabbixQueryOptionsController {}\nZabbixQueryOptionsController.templateUrl = 'datasource-zabbix/partials/query.options.html';\n\nclass ZabbixAnnotationsQueryController {}\nZabbixAnnotationsQueryController.templateUrl = 'datasource-zabbix/partials/annotations.editor.html';\n\nexport {\n ZabbixAPIDatasource as Datasource,\n ZabbixConfigController as ConfigCtrl,\n ZabbixQueryController as QueryCtrl,\n ZabbixQueryOptionsController as QueryOptionsCtrl,\n ZabbixAnnotationsQueryController as AnnotationsQueryCtrl\n};\n"]}
|
||||
{"version":3,"sources":["../../src/datasource-zabbix/module.js"],"names":["ZabbixAPIDatasource","ZabbixQueryController","ZabbixDSConfigController","ZabbixQueryOptionsController","templateUrl","ZabbixAnnotationsQueryController"],"mappings":";;;;;;;;;;;;;;;AAAQA,yB,eAAAA,mB;;AACAC,2B,oBAAAA,qB;;AACAC,8B,qBAAAA,wB;;;kCAEFC,4B;;;;AACNA,mCAA6BC,WAA7B,GAA2C,+CAA3C;;sCAEMC,gC;;;;AACNA,uCAAiCD,WAAjC,GAA+C,oDAA/C;;4BAGEJ,mB;;4BACAE,wB;;2BACAD,qB;;kCACAE,4B;;sCACAE,gC","file":"module.js","sourcesContent":["import {ZabbixAPIDatasource} from './datasource';\nimport {ZabbixQueryController} from './query.controller';\nimport {ZabbixDSConfigController} from './config.controller';\n\nclass ZabbixQueryOptionsController {}\nZabbixQueryOptionsController.templateUrl = 'datasource-zabbix/partials/query.options.html';\n\nclass ZabbixAnnotationsQueryController {}\nZabbixAnnotationsQueryController.templateUrl = 'datasource-zabbix/partials/annotations.editor.html';\n\nexport {\n ZabbixAPIDatasource as Datasource,\n ZabbixDSConfigController as ConfigCtrl,\n ZabbixQueryController as QueryCtrl,\n ZabbixQueryOptionsController as QueryOptionsCtrl,\n ZabbixAnnotationsQueryController as AnnotationsQueryCtrl\n};\n"]}
|
||||
35
dist/datasource-zabbix/partials/config.html
vendored
35
dist/datasource-zabbix/partials/config.html
vendored
@@ -75,18 +75,46 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h3 class="page-heading">Direct DB Connection</h3>
|
||||
<gf-form-switch class="gf-form" label-class="width-12"
|
||||
label="Enable"
|
||||
checked="ctrl.current.jsonData.dbConnection.enable">
|
||||
</gf-form-switch>
|
||||
<div ng-if="ctrl.current.jsonData.dbConnection.enable">
|
||||
<div class="gf-form max-width-20">
|
||||
<span class="gf-form-label width-12">
|
||||
SQL Data Source
|
||||
<info-popover mode="right-normal">
|
||||
Select SQL Data Source for Zabbix database.
|
||||
In order to use this feature you should <a href="/datasources/new" target="_blank">create</a> and
|
||||
configure it first. Zabbix plugin uses this data source for querying history data directly from database.
|
||||
This way usually faster than pulling data from Zabbix API, especially on the wide time ranges, and reduces
|
||||
amount of data transfered.
|
||||
</info-popover>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper max-width-16">
|
||||
<select class="gf-form-input" ng-model="ctrl.current.jsonData.dbConnection.datasourceId"
|
||||
ng-options="ds.id as ds.name for ds in ctrl.sqlDataSources">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h3 class="page-heading">Alerting</h3>
|
||||
<gf-form-switch class="gf-form" label-class="width-9"
|
||||
<gf-form-switch class="gf-form" label-class="width-12"
|
||||
label="Enable alerting"
|
||||
checked="ctrl.current.jsonData.alerting">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label-class="width-9"
|
||||
<div ng-if="ctrl.current.jsonData.alerting">
|
||||
<gf-form-switch class="gf-form" label-class="width-12"
|
||||
label="Add thresholds"
|
||||
checked="ctrl.current.jsonData.addThresholds">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form max-width-20">
|
||||
<span class="gf-form-label width-9">Min severity</span>
|
||||
<span class="gf-form-label width-12">Min severity</span>
|
||||
<div class="gf-form-select-wrapper max-width-16">
|
||||
<select class="gf-form-input" ng-model="ctrl.current.jsonData.alertingMinSeverity"
|
||||
ng-options="s.val as s.text for s in [
|
||||
@@ -96,4 +124,5 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,10 @@ describe('ZabbixDatasource', () => {
|
||||
password: 'zabbix',
|
||||
trends: true,
|
||||
trendsFrom: '14d',
|
||||
trendsRange: '7d'
|
||||
trendsRange: '7d',
|
||||
dbConnection: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
};
|
||||
ctx.templateSrv = {};
|
||||
|
||||
28
dist/datasource-zabbix/zabbix.js
vendored
28
dist/datasource-zabbix/zabbix.js
vendored
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
System.register(['angular', 'lodash', './utils', './zabbixAPI.service.js', './zabbixCachingProxy.service.js'], function (_export, _context) {
|
||||
System.register(['angular', 'lodash', './utils', './zabbixAPI.service.js', './zabbixCachingProxy.service.js', './zabbixDBConnector'], function (_export, _context) {
|
||||
"use strict";
|
||||
|
||||
var angular, _, utils, _createClass;
|
||||
@@ -27,26 +27,44 @@ System.register(['angular', 'lodash', './utils', './zabbixAPI.service.js', './za
|
||||
// Each Zabbix data source instance should initialize its own API instance.
|
||||
|
||||
/** @ngInject */
|
||||
function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy) {
|
||||
function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy, ZabbixDBConnector) {
|
||||
var Zabbix = function () {
|
||||
function Zabbix(url, username, password, basicAuth, withCredentials, cacheTTL) {
|
||||
function Zabbix(url, options) {
|
||||
_classCallCheck(this, Zabbix);
|
||||
|
||||
var username = options.username,
|
||||
password = options.password,
|
||||
basicAuth = options.basicAuth,
|
||||
withCredentials = options.withCredentials,
|
||||
cacheTTL = options.cacheTTL,
|
||||
enableDirectDBConnection = options.enableDirectDBConnection,
|
||||
sqlDatasourceId = options.sqlDatasourceId;
|
||||
|
||||
|
||||
// Initialize Zabbix API
|
||||
var ZabbixAPI = zabbixAPIService;
|
||||
this.zabbixAPI = new ZabbixAPI(url, username, password, basicAuth, withCredentials);
|
||||
|
||||
if (enableDirectDBConnection) {
|
||||
this.dbConnector = new ZabbixDBConnector(sqlDatasourceId);
|
||||
}
|
||||
|
||||
// Initialize caching proxy for requests
|
||||
var cacheOptions = {
|
||||
enabled: true,
|
||||
ttl: cacheTTL
|
||||
};
|
||||
this.cachingProxy = new ZabbixCachingProxy(this.zabbixAPI, cacheOptions);
|
||||
this.cachingProxy = new ZabbixCachingProxy(this.zabbixAPI, this.dbConnector, cacheOptions);
|
||||
|
||||
// Proxy methods
|
||||
this.getHistory = this.cachingProxy.getHistory.bind(this.cachingProxy);
|
||||
this.getMacros = this.cachingProxy.getMacros.bind(this.cachingProxy);
|
||||
|
||||
if (enableDirectDBConnection) {
|
||||
this.getHistoryDB = this.cachingProxy.getHistoryDB.bind(this.cachingProxy);
|
||||
this.getTrendsDB = this.cachingProxy.getTrendsDB.bind(this.cachingProxy);
|
||||
}
|
||||
|
||||
this.getTrend = this.zabbixAPI.getTrend.bind(this.zabbixAPI);
|
||||
this.getEvents = this.zabbixAPI.getEvents.bind(this.zabbixAPI);
|
||||
this.getAlerts = this.zabbixAPI.getAlerts.bind(this.zabbixAPI);
|
||||
@@ -274,7 +292,7 @@ System.register(['angular', 'lodash', './utils', './zabbixAPI.service.js', './za
|
||||
_ = _lodash.default;
|
||||
}, function (_utils) {
|
||||
utils = _utils;
|
||||
}, function (_zabbixAPIServiceJs) {}, function (_zabbixCachingProxyServiceJs) {}],
|
||||
}, function (_zabbixAPIServiceJs) {}, function (_zabbixCachingProxyServiceJs) {}, function (_zabbixDBConnector) {}],
|
||||
execute: function () {
|
||||
_createClass = function () {
|
||||
function defineProperties(target, props) {
|
||||
|
||||
2
dist/datasource-zabbix/zabbix.js.map
vendored
2
dist/datasource-zabbix/zabbix.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -29,10 +29,11 @@ System.register(['angular', 'lodash'], function (_export, _context) {
|
||||
/** @ngInject */
|
||||
function ZabbixCachingProxyFactory() {
|
||||
var ZabbixCachingProxy = function () {
|
||||
function ZabbixCachingProxy(zabbixAPI, cacheOptions) {
|
||||
function ZabbixCachingProxy(zabbixAPI, zabbixDBConnector, cacheOptions) {
|
||||
_classCallCheck(this, ZabbixCachingProxy);
|
||||
|
||||
this.zabbixAPI = zabbixAPI;
|
||||
this.dbConnector = zabbixDBConnector;
|
||||
this.cacheEnabled = cacheOptions.enabled;
|
||||
this.ttl = cacheOptions.ttl || 600000; // 10 minutes by default
|
||||
|
||||
@@ -53,6 +54,11 @@ System.register(['angular', 'lodash'], function (_export, _context) {
|
||||
// Don't run duplicated history requests
|
||||
this.getHistory = callAPIRequestOnce(_.bind(this.zabbixAPI.getHistory, this.zabbixAPI), this.historyPromises, getHistoryRequestHash);
|
||||
|
||||
if (this.dbConnector) {
|
||||
this.getHistoryDB = callAPIRequestOnce(_.bind(this.dbConnector.getHistory, this.dbConnector), this.historyPromises, getDBQueryHash);
|
||||
this.getTrendsDB = callAPIRequestOnce(_.bind(this.dbConnector.getTrends, this.dbConnector), this.historyPromises, getDBQueryHash);
|
||||
}
|
||||
|
||||
// Don't run duplicated requests
|
||||
this.groupPromises = {};
|
||||
this.getGroupsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getGroups, this.zabbixAPI), this.groupPromises, getRequestHash);
|
||||
@@ -209,6 +215,14 @@ System.register(['angular', 'lodash'], function (_export, _context) {
|
||||
return stamp.getHash();
|
||||
}
|
||||
|
||||
function getDBQueryHash(args) {
|
||||
var itemids = _.map(args[0], 'itemid');
|
||||
var consolidateBy = args[3].consolidateBy;
|
||||
var intervalMs = args[3].intervalMs;
|
||||
var stamp = itemids.join() + args[1] + args[2] + consolidateBy + intervalMs;
|
||||
return stamp.getHash();
|
||||
}
|
||||
|
||||
return {
|
||||
setters: [function (_angular) {
|
||||
angular = _angular.default;
|
||||
|
||||
File diff suppressed because one or more lines are too long
224
dist/datasource-zabbix/zabbixDBConnector.js
vendored
Normal file
224
dist/datasource-zabbix/zabbixDBConnector.js
vendored
Normal file
@@ -0,0 +1,224 @@
|
||||
'use strict';
|
||||
|
||||
System.register(['angular', 'lodash'], function (_export, _context) {
|
||||
"use strict";
|
||||
|
||||
var angular, _, _createClass, DEFAULT_QUERY_LIMIT, HISTORY_TO_TABLE_MAP, TREND_TO_TABLE_MAP, consolidateByFunc, consolidateByTrendColumns;
|
||||
|
||||
function _classCallCheck(instance, Constructor) {
|
||||
if (!(instance instanceof Constructor)) {
|
||||
throw new TypeError("Cannot call a class as a function");
|
||||
}
|
||||
}
|
||||
|
||||
/** @ngInject */
|
||||
function ZabbixDBConnectorFactory(datasourceSrv, backendSrv) {
|
||||
var ZabbixDBConnector = function () {
|
||||
function ZabbixDBConnector(sqlDataSourceId) {
|
||||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
||||
|
||||
_classCallCheck(this, ZabbixDBConnector);
|
||||
|
||||
var limit = options.limit;
|
||||
|
||||
|
||||
this.sqlDataSourceId = sqlDataSourceId;
|
||||
this.limit = limit || DEFAULT_QUERY_LIMIT;
|
||||
|
||||
// Try to load DS with given id to check it's exist
|
||||
this.loadSQLDataSource(sqlDataSourceId);
|
||||
}
|
||||
|
||||
_createClass(ZabbixDBConnector, [{
|
||||
key: 'loadSQLDataSource',
|
||||
value: function loadSQLDataSource(datasourceId) {
|
||||
var ds = _.find(datasourceSrv.getAll(), { 'id': datasourceId });
|
||||
if (ds) {
|
||||
return datasourceSrv.loadDatasource(ds.name).then(function (ds) {
|
||||
console.log('SQL data source loaded', ds);
|
||||
});
|
||||
} else {
|
||||
return Promise.reject('SQL Data Source with ID ' + datasourceId + ' not found');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: 'getHistory',
|
||||
value: function getHistory(items, timeFrom, timeTill, options) {
|
||||
var _this = this;
|
||||
|
||||
var intervalMs = options.intervalMs,
|
||||
consolidateBy = options.consolidateBy;
|
||||
|
||||
var intervalSec = Math.ceil(intervalMs / 1000);
|
||||
|
||||
consolidateBy = consolidateBy || 'avg';
|
||||
var aggFunction = consolidateByFunc[consolidateBy];
|
||||
|
||||
// Group items by value type and perform request for each value type
|
||||
var grouped_items = _.groupBy(items, 'value_type');
|
||||
var promises = _.map(grouped_items, function (items, value_type) {
|
||||
var itemids = _.map(items, 'itemid').join(', ');
|
||||
var table = HISTORY_TO_TABLE_MAP[value_type];
|
||||
|
||||
var query = '\n SELECT itemid AS metric, clock AS time_sec, ' + aggFunction + '(value) as value\n FROM ' + table + '\n WHERE itemid IN (' + itemids + ')\n AND clock > ' + timeFrom + ' AND clock < ' + timeTill + '\n GROUP BY time_sec DIV ' + intervalSec + ', metric\n ';
|
||||
|
||||
query = compactSQLQuery(query);
|
||||
return _this.invokeSQLQuery(query);
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(function (results) {
|
||||
return _.flatten(results);
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: 'getTrends',
|
||||
value: function getTrends(items, timeFrom, timeTill, options) {
|
||||
var _this2 = this;
|
||||
|
||||
var intervalMs = options.intervalMs,
|
||||
consolidateBy = options.consolidateBy;
|
||||
|
||||
var intervalSec = Math.ceil(intervalMs / 1000);
|
||||
|
||||
consolidateBy = consolidateBy || 'avg';
|
||||
var aggFunction = consolidateByFunc[consolidateBy];
|
||||
|
||||
// Group items by value type and perform request for each value type
|
||||
var grouped_items = _.groupBy(items, 'value_type');
|
||||
var promises = _.map(grouped_items, function (items, value_type) {
|
||||
var itemids = _.map(items, 'itemid').join(', ');
|
||||
var table = TREND_TO_TABLE_MAP[value_type];
|
||||
var valueColumn = _.includes(['avg', 'min', 'max'], consolidateBy) ? consolidateBy : 'avg';
|
||||
valueColumn = consolidateByTrendColumns[valueColumn];
|
||||
|
||||
var query = '\n SELECT itemid AS metric, clock AS time_sec, ' + aggFunction + '(' + valueColumn + ') as value\n FROM ' + table + '\n WHERE itemid IN (' + itemids + ')\n AND clock > ' + timeFrom + ' AND clock < ' + timeTill + '\n GROUP BY time_sec DIV ' + intervalSec + ', metric\n ';
|
||||
|
||||
query = compactSQLQuery(query);
|
||||
return _this2.invokeSQLQuery(query);
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(function (results) {
|
||||
return _.flatten(results);
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: 'handleGrafanaTSResponse',
|
||||
value: function handleGrafanaTSResponse(history, items) {
|
||||
var addHostName = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
|
||||
|
||||
return convertGrafanaTSResponse(history, items, addHostName);
|
||||
}
|
||||
}, {
|
||||
key: 'invokeSQLQuery',
|
||||
value: function invokeSQLQuery(query) {
|
||||
var queryDef = {
|
||||
refId: 'A',
|
||||
format: 'time_series',
|
||||
datasourceId: this.sqlDataSourceId,
|
||||
rawSql: query,
|
||||
maxDataPoints: this.limit
|
||||
};
|
||||
|
||||
return backendSrv.datasourceRequest({
|
||||
url: '/api/tsdb/query',
|
||||
method: 'POST',
|
||||
data: {
|
||||
queries: [queryDef]
|
||||
}
|
||||
}).then(function (response) {
|
||||
var results = response.data.results;
|
||||
if (results['A']) {
|
||||
return results['A'].series;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}]);
|
||||
|
||||
return ZabbixDBConnector;
|
||||
}();
|
||||
|
||||
return ZabbixDBConnector;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function convertGrafanaTSResponse(time_series, items, addHostName) {
|
||||
var hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate
|
||||
var grafanaSeries = _.map(time_series, function (series) {
|
||||
var itemid = series.name;
|
||||
var datapoints = series.points;
|
||||
var item = _.find(items, { 'itemid': itemid });
|
||||
var alias = item.name;
|
||||
if (_.keys(hosts).length > 1 && addHostName) {
|
||||
//only when actual multi hosts selected
|
||||
var host = _.find(hosts, { 'hostid': item.hostid });
|
||||
alias = host.name + ": " + alias;
|
||||
}
|
||||
return {
|
||||
target: alias,
|
||||
datapoints: datapoints
|
||||
};
|
||||
});
|
||||
|
||||
return _.sortBy(grafanaSeries, 'target');
|
||||
}
|
||||
|
||||
function compactSQLQuery(query) {
|
||||
return query.replace(/\s+/g, ' ');
|
||||
}
|
||||
return {
|
||||
setters: [function (_angular) {
|
||||
angular = _angular.default;
|
||||
}, function (_lodash) {
|
||||
_ = _lodash.default;
|
||||
}],
|
||||
execute: function () {
|
||||
_createClass = function () {
|
||||
function defineProperties(target, props) {
|
||||
for (var i = 0; i < props.length; i++) {
|
||||
var descriptor = props[i];
|
||||
descriptor.enumerable = descriptor.enumerable || false;
|
||||
descriptor.configurable = true;
|
||||
if ("value" in descriptor) descriptor.writable = true;
|
||||
Object.defineProperty(target, descriptor.key, descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
return function (Constructor, protoProps, staticProps) {
|
||||
if (protoProps) defineProperties(Constructor.prototype, protoProps);
|
||||
if (staticProps) defineProperties(Constructor, staticProps);
|
||||
return Constructor;
|
||||
};
|
||||
}();
|
||||
|
||||
DEFAULT_QUERY_LIMIT = 10000;
|
||||
HISTORY_TO_TABLE_MAP = {
|
||||
'0': 'history',
|
||||
'1': 'history_str',
|
||||
'2': 'history_log',
|
||||
'3': 'history_uint',
|
||||
'4': 'history_text'
|
||||
};
|
||||
TREND_TO_TABLE_MAP = {
|
||||
'0': 'trends',
|
||||
'3': 'trends_uint'
|
||||
};
|
||||
consolidateByFunc = {
|
||||
'avg': 'AVG',
|
||||
'min': 'MIN',
|
||||
'max': 'MAX',
|
||||
'sum': 'SUM',
|
||||
'count': 'COUNT'
|
||||
};
|
||||
consolidateByTrendColumns = {
|
||||
'avg': 'value_avg',
|
||||
'min': 'value_min',
|
||||
'max': 'value_max'
|
||||
};
|
||||
angular.module('grafana.services').factory('ZabbixDBConnector', ZabbixDBConnectorFactory);
|
||||
}
|
||||
};
|
||||
});
|
||||
//# sourceMappingURL=zabbixDBConnector.js.map
|
||||
1
dist/datasource-zabbix/zabbixDBConnector.js.map
vendored
Normal file
1
dist/datasource-zabbix/zabbixDBConnector.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
50
dist/test/datasource-zabbix/config.controller.js
vendored
Normal file
50
dist/test/datasource-zabbix/config.controller.js
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.ZabbixDSConfigController = undefined;
|
||||
|
||||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
|
||||
|
||||
var _lodash = require('lodash');
|
||||
|
||||
var _lodash2 = _interopRequireDefault(_lodash);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
||||
|
||||
var SUPPORTED_SQL_DS = ['mysql'];
|
||||
|
||||
var defaultConfig = {
|
||||
dbConnection: {
|
||||
enable: false
|
||||
}
|
||||
};
|
||||
|
||||
var ZabbixDSConfigController = exports.ZabbixDSConfigController = function () {
|
||||
/** @ngInject */
|
||||
function ZabbixDSConfigController($scope, $injector, datasourceSrv) {
|
||||
_classCallCheck(this, ZabbixDSConfigController);
|
||||
|
||||
this.datasourceSrv = datasourceSrv;
|
||||
|
||||
_lodash2.default.defaults(this.current.jsonData, defaultConfig);
|
||||
this.sqlDataSources = this.getSupportedSQLDataSources();
|
||||
}
|
||||
|
||||
_createClass(ZabbixDSConfigController, [{
|
||||
key: 'getSupportedSQLDataSources',
|
||||
value: function getSupportedSQLDataSources() {
|
||||
var datasources = this.datasourceSrv.getAll();
|
||||
return _lodash2.default.filter(datasources, function (ds) {
|
||||
return _lodash2.default.includes(SUPPORTED_SQL_DS, ds.type);
|
||||
});
|
||||
}
|
||||
}]);
|
||||
|
||||
return ZabbixDSConfigController;
|
||||
}();
|
||||
|
||||
ZabbixDSConfigController.templateUrl = 'datasource-zabbix/partials/config.html';
|
||||
49
dist/test/datasource-zabbix/datasource.js
vendored
49
dist/test/datasource-zabbix/datasource.js
vendored
@@ -64,6 +64,9 @@ var ZabbixAPIDatasource = function () {
|
||||
this.dashboardSrv = dashboardSrv;
|
||||
this.zabbixAlertingSrv = zabbixAlertingSrv;
|
||||
|
||||
// Use custom format for template variables
|
||||
this.replaceTemplateVars = _lodash2.default.partial(replaceTemplateVars, this.templateSrv);
|
||||
|
||||
// General data source settings
|
||||
this.name = instanceSettings.name;
|
||||
this.url = instanceSettings.url;
|
||||
@@ -88,10 +91,21 @@ var ZabbixAPIDatasource = function () {
|
||||
this.addThresholds = instanceSettings.jsonData.addThresholds;
|
||||
this.alertingMinSeverity = instanceSettings.jsonData.alertingMinSeverity || c.SEV_WARNING;
|
||||
|
||||
this.zabbix = new Zabbix(this.url, this.username, this.password, this.basicAuth, this.withCredentials, this.cacheTTL);
|
||||
// Direct DB Connection options
|
||||
this.enableDirectDBConnection = instanceSettings.jsonData.dbConnection.enable;
|
||||
this.sqlDatasourceId = instanceSettings.jsonData.dbConnection.datasourceId;
|
||||
|
||||
// Use custom format for template variables
|
||||
this.replaceTemplateVars = _lodash2.default.partial(replaceTemplateVars, this.templateSrv);
|
||||
var zabbixOptions = {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
basicAuth: this.basicAuth,
|
||||
withCredentials: this.withCredentials,
|
||||
cacheTTL: this.cacheTTL,
|
||||
enableDirectDBConnection: this.enableDirectDBConnection,
|
||||
sqlDatasourceId: this.sqlDatasourceId
|
||||
};
|
||||
|
||||
this.zabbix = new Zabbix(this.url, zabbixOptions);
|
||||
}
|
||||
|
||||
////////////////////////
|
||||
@@ -197,8 +211,14 @@ var ZabbixAPIDatasource = function () {
|
||||
};
|
||||
return this.zabbix.getItemsFromTarget(target, getItemOptions).then(function (items) {
|
||||
var getHistoryPromise = void 0;
|
||||
options.consolidateBy = getConsolidateBy(target);
|
||||
|
||||
if (useTrends) {
|
||||
if (_this2.enableDirectDBConnection) {
|
||||
getHistoryPromise = _this2.zabbix.getTrendsDB(items, timeFrom, timeTo, options).then(function (history) {
|
||||
return _this2.zabbix.dbConnector.handleGrafanaTSResponse(history, items);
|
||||
});
|
||||
} else {
|
||||
var valueType = _this2.getTrendValueType(target);
|
||||
getHistoryPromise = _this2.zabbix.getTrend(items, timeFrom, timeTo).then(function (history) {
|
||||
return _responseHandler2.default.handleTrends(history, items, valueType);
|
||||
@@ -209,15 +229,21 @@ var ZabbixAPIDatasource = function () {
|
||||
return point[c.DATAPOINT_TS];
|
||||
});
|
||||
});
|
||||
|
||||
return timeseries;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Use history
|
||||
if (_this2.enableDirectDBConnection) {
|
||||
getHistoryPromise = _this2.zabbix.getHistoryDB(items, timeFrom, timeTo, options).then(function (history) {
|
||||
return _this2.zabbix.dbConnector.handleGrafanaTSResponse(history, items);
|
||||
});
|
||||
} else {
|
||||
getHistoryPromise = _this2.zabbix.getHistory(items, timeFrom, timeTo).then(function (history) {
|
||||
return _responseHandler2.default.handleHistory(history, items);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return getHistoryPromise;
|
||||
}).then(function (timeseries) {
|
||||
@@ -593,10 +619,23 @@ function bindFunctionDefs(functionDefs, category) {
|
||||
});
|
||||
}
|
||||
|
||||
function getConsolidateBy(target) {
|
||||
var consolidateBy = 'avg';
|
||||
var funcDef = _lodash2.default.find(target.functions, function (func) {
|
||||
return func.def.name === 'consolidateBy';
|
||||
});
|
||||
if (funcDef && funcDef.params && funcDef.params.length) {
|
||||
consolidateBy = funcDef.params[0];
|
||||
}
|
||||
return consolidateBy;
|
||||
}
|
||||
|
||||
function downsampleSeries(timeseries_data, options) {
|
||||
var defaultAgg = _dataProcessor2.default.aggregationFunctions['avg'];
|
||||
var consolidateByFunc = _dataProcessor2.default.aggregationFunctions[options.consolidateBy] || defaultAgg;
|
||||
return _lodash2.default.map(timeseries_data, function (timeseries) {
|
||||
if (timeseries.datapoints.length > options.maxDataPoints) {
|
||||
timeseries.datapoints = _dataProcessor2.default.groupBy(options.interval, _dataProcessor2.default.AVERAGE, timeseries.datapoints);
|
||||
timeseries.datapoints = _dataProcessor2.default.groupBy(options.interval, consolidateByFunc, timeseries.datapoints);
|
||||
}
|
||||
return timeseries;
|
||||
});
|
||||
|
||||
11
dist/test/datasource-zabbix/metricFunctions.js
vendored
11
dist/test/datasource-zabbix/metricFunctions.js
vendored
@@ -29,7 +29,8 @@ var categories = {
|
||||
Filter: [],
|
||||
Trends: [],
|
||||
Time: [],
|
||||
Alias: []
|
||||
Alias: [],
|
||||
Special: []
|
||||
};
|
||||
|
||||
function addFuncDef(funcDef) {
|
||||
@@ -188,6 +189,14 @@ addFuncDef({
|
||||
defaultParams: ['/(.*)/', '$1']
|
||||
});
|
||||
|
||||
// Special
|
||||
addFuncDef({
|
||||
name: 'consolidateBy',
|
||||
category: 'Special',
|
||||
params: [{ name: 'type', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count'] }],
|
||||
defaultParams: ['avg']
|
||||
});
|
||||
|
||||
_lodash2.default.each(categories, function (funcList, catName) {
|
||||
categories[catName] = _lodash2.default.sortBy(funcList, 'name');
|
||||
});
|
||||
|
||||
10
dist/test/datasource-zabbix/module.js
vendored
10
dist/test/datasource-zabbix/module.js
vendored
@@ -9,14 +9,10 @@ var _datasource = require('./datasource');
|
||||
|
||||
var _query = require('./query.controller');
|
||||
|
||||
var _config = require('./config.controller');
|
||||
|
||||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
||||
|
||||
var ZabbixConfigController = function ZabbixConfigController() {
|
||||
_classCallCheck(this, ZabbixConfigController);
|
||||
};
|
||||
|
||||
ZabbixConfigController.templateUrl = 'datasource-zabbix/partials/config.html';
|
||||
|
||||
var ZabbixQueryOptionsController = function ZabbixQueryOptionsController() {
|
||||
_classCallCheck(this, ZabbixQueryOptionsController);
|
||||
};
|
||||
@@ -30,7 +26,7 @@ var ZabbixAnnotationsQueryController = function ZabbixAnnotationsQueryController
|
||||
ZabbixAnnotationsQueryController.templateUrl = 'datasource-zabbix/partials/annotations.editor.html';
|
||||
|
||||
exports.Datasource = _datasource.ZabbixAPIDatasource;
|
||||
exports.ConfigCtrl = ZabbixConfigController;
|
||||
exports.ConfigCtrl = _config.ZabbixDSConfigController;
|
||||
exports.QueryCtrl = _query.ZabbixQueryController;
|
||||
exports.QueryOptionsCtrl = ZabbixQueryOptionsController;
|
||||
exports.AnnotationsQueryCtrl = ZabbixAnnotationsQueryController;
|
||||
|
||||
@@ -29,7 +29,10 @@ describe('ZabbixDatasource', function () {
|
||||
password: 'zabbix',
|
||||
trends: true,
|
||||
trendsFrom: '14d',
|
||||
trendsRange: '7d'
|
||||
trendsRange: '7d',
|
||||
dbConnection: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
};
|
||||
ctx.templateSrv = {};
|
||||
|
||||
26
dist/test/datasource-zabbix/zabbix.js
vendored
26
dist/test/datasource-zabbix/zabbix.js
vendored
@@ -18,6 +18,8 @@ require('./zabbixAPI.service.js');
|
||||
|
||||
require('./zabbixCachingProxy.service.js');
|
||||
|
||||
require('./zabbixDBConnector');
|
||||
|
||||
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
@@ -30,26 +32,44 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons
|
||||
// Each Zabbix data source instance should initialize its own API instance.
|
||||
|
||||
/** @ngInject */
|
||||
function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy) {
|
||||
function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy, ZabbixDBConnector) {
|
||||
var Zabbix = function () {
|
||||
function Zabbix(url, username, password, basicAuth, withCredentials, cacheTTL) {
|
||||
function Zabbix(url, options) {
|
||||
_classCallCheck(this, Zabbix);
|
||||
|
||||
var username = options.username,
|
||||
password = options.password,
|
||||
basicAuth = options.basicAuth,
|
||||
withCredentials = options.withCredentials,
|
||||
cacheTTL = options.cacheTTL,
|
||||
enableDirectDBConnection = options.enableDirectDBConnection,
|
||||
sqlDatasourceId = options.sqlDatasourceId;
|
||||
|
||||
// Initialize Zabbix API
|
||||
|
||||
var ZabbixAPI = zabbixAPIService;
|
||||
this.zabbixAPI = new ZabbixAPI(url, username, password, basicAuth, withCredentials);
|
||||
|
||||
if (enableDirectDBConnection) {
|
||||
this.dbConnector = new ZabbixDBConnector(sqlDatasourceId);
|
||||
}
|
||||
|
||||
// Initialize caching proxy for requests
|
||||
var cacheOptions = {
|
||||
enabled: true,
|
||||
ttl: cacheTTL
|
||||
};
|
||||
this.cachingProxy = new ZabbixCachingProxy(this.zabbixAPI, cacheOptions);
|
||||
this.cachingProxy = new ZabbixCachingProxy(this.zabbixAPI, this.dbConnector, cacheOptions);
|
||||
|
||||
// Proxy methods
|
||||
this.getHistory = this.cachingProxy.getHistory.bind(this.cachingProxy);
|
||||
this.getMacros = this.cachingProxy.getMacros.bind(this.cachingProxy);
|
||||
|
||||
if (enableDirectDBConnection) {
|
||||
this.getHistoryDB = this.cachingProxy.getHistoryDB.bind(this.cachingProxy);
|
||||
this.getTrendsDB = this.cachingProxy.getTrendsDB.bind(this.cachingProxy);
|
||||
}
|
||||
|
||||
this.getTrend = this.zabbixAPI.getTrend.bind(this.zabbixAPI);
|
||||
this.getEvents = this.zabbixAPI.getEvents.bind(this.zabbixAPI);
|
||||
this.getAlerts = this.zabbixAPI.getAlerts.bind(this.zabbixAPI);
|
||||
|
||||
@@ -22,10 +22,11 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons
|
||||
/** @ngInject */
|
||||
function ZabbixCachingProxyFactory() {
|
||||
var ZabbixCachingProxy = function () {
|
||||
function ZabbixCachingProxy(zabbixAPI, cacheOptions) {
|
||||
function ZabbixCachingProxy(zabbixAPI, zabbixDBConnector, cacheOptions) {
|
||||
_classCallCheck(this, ZabbixCachingProxy);
|
||||
|
||||
this.zabbixAPI = zabbixAPI;
|
||||
this.dbConnector = zabbixDBConnector;
|
||||
this.cacheEnabled = cacheOptions.enabled;
|
||||
this.ttl = cacheOptions.ttl || 600000; // 10 minutes by default
|
||||
|
||||
@@ -46,6 +47,11 @@ function ZabbixCachingProxyFactory() {
|
||||
// Don't run duplicated history requests
|
||||
this.getHistory = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getHistory, this.zabbixAPI), this.historyPromises, getHistoryRequestHash);
|
||||
|
||||
if (this.dbConnector) {
|
||||
this.getHistoryDB = callAPIRequestOnce(_lodash2.default.bind(this.dbConnector.getHistory, this.dbConnector), this.historyPromises, getDBQueryHash);
|
||||
this.getTrendsDB = callAPIRequestOnce(_lodash2.default.bind(this.dbConnector.getTrends, this.dbConnector), this.historyPromises, getDBQueryHash);
|
||||
}
|
||||
|
||||
// Don't run duplicated requests
|
||||
this.groupPromises = {};
|
||||
this.getGroupsOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getGroups, this.zabbixAPI), this.groupPromises, getRequestHash);
|
||||
@@ -210,6 +216,14 @@ function getHistoryRequestHash(args) {
|
||||
return stamp.getHash();
|
||||
}
|
||||
|
||||
function getDBQueryHash(args) {
|
||||
var itemids = _lodash2.default.map(args[0], 'itemid');
|
||||
var consolidateBy = args[3].consolidateBy;
|
||||
var intervalMs = args[3].intervalMs;
|
||||
var stamp = itemids.join() + args[1] + args[2] + consolidateBy + intervalMs;
|
||||
return stamp.getHash();
|
||||
}
|
||||
|
||||
String.prototype.getHash = function () {
|
||||
var hash = 0,
|
||||
i,
|
||||
|
||||
203
dist/test/datasource-zabbix/zabbixDBConnector.js
vendored
Normal file
203
dist/test/datasource-zabbix/zabbixDBConnector.js
vendored
Normal file
@@ -0,0 +1,203 @@
|
||||
'use strict';
|
||||
|
||||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
|
||||
|
||||
var _angular = require('angular');
|
||||
|
||||
var _angular2 = _interopRequireDefault(_angular);
|
||||
|
||||
var _lodash = require('lodash');
|
||||
|
||||
var _lodash2 = _interopRequireDefault(_lodash);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
||||
|
||||
var DEFAULT_QUERY_LIMIT = 10000;
|
||||
var HISTORY_TO_TABLE_MAP = {
|
||||
'0': 'history',
|
||||
'1': 'history_str',
|
||||
'2': 'history_log',
|
||||
'3': 'history_uint',
|
||||
'4': 'history_text'
|
||||
};
|
||||
|
||||
var TREND_TO_TABLE_MAP = {
|
||||
'0': 'trends',
|
||||
'3': 'trends_uint'
|
||||
};
|
||||
|
||||
var consolidateByFunc = {
|
||||
'avg': 'AVG',
|
||||
'min': 'MIN',
|
||||
'max': 'MAX',
|
||||
'sum': 'SUM',
|
||||
'count': 'COUNT'
|
||||
};
|
||||
|
||||
var consolidateByTrendColumns = {
|
||||
'avg': 'value_avg',
|
||||
'min': 'value_min',
|
||||
'max': 'value_max'
|
||||
};
|
||||
|
||||
/** @ngInject */
|
||||
function ZabbixDBConnectorFactory(datasourceSrv, backendSrv) {
|
||||
var ZabbixDBConnector = function () {
|
||||
function ZabbixDBConnector(sqlDataSourceId) {
|
||||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
||||
|
||||
_classCallCheck(this, ZabbixDBConnector);
|
||||
|
||||
var limit = options.limit;
|
||||
|
||||
|
||||
this.sqlDataSourceId = sqlDataSourceId;
|
||||
this.limit = limit || DEFAULT_QUERY_LIMIT;
|
||||
|
||||
// Try to load DS with given id to check it's exist
|
||||
this.loadSQLDataSource(sqlDataSourceId);
|
||||
}
|
||||
|
||||
_createClass(ZabbixDBConnector, [{
|
||||
key: 'loadSQLDataSource',
|
||||
value: function loadSQLDataSource(datasourceId) {
|
||||
var ds = _lodash2.default.find(datasourceSrv.getAll(), { 'id': datasourceId });
|
||||
if (ds) {
|
||||
return datasourceSrv.loadDatasource(ds.name).then(function (ds) {
|
||||
console.log('SQL data source loaded', ds);
|
||||
});
|
||||
} else {
|
||||
return Promise.reject('SQL Data Source with ID ' + datasourceId + ' not found');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: 'getHistory',
|
||||
value: function getHistory(items, timeFrom, timeTill, options) {
|
||||
var _this = this;
|
||||
|
||||
var intervalMs = options.intervalMs,
|
||||
consolidateBy = options.consolidateBy;
|
||||
|
||||
var intervalSec = Math.ceil(intervalMs / 1000);
|
||||
|
||||
consolidateBy = consolidateBy || 'avg';
|
||||
var aggFunction = consolidateByFunc[consolidateBy];
|
||||
|
||||
// Group items by value type and perform request for each value type
|
||||
var grouped_items = _lodash2.default.groupBy(items, 'value_type');
|
||||
var promises = _lodash2.default.map(grouped_items, function (items, value_type) {
|
||||
var itemids = _lodash2.default.map(items, 'itemid').join(', ');
|
||||
var table = HISTORY_TO_TABLE_MAP[value_type];
|
||||
|
||||
var query = '\n SELECT itemid AS metric, clock AS time_sec, ' + aggFunction + '(value) as value\n FROM ' + table + '\n WHERE itemid IN (' + itemids + ')\n AND clock > ' + timeFrom + ' AND clock < ' + timeTill + '\n GROUP BY time_sec DIV ' + intervalSec + ', metric\n ';
|
||||
|
||||
query = compactSQLQuery(query);
|
||||
return _this.invokeSQLQuery(query);
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(function (results) {
|
||||
return _lodash2.default.flatten(results);
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: 'getTrends',
|
||||
value: function getTrends(items, timeFrom, timeTill, options) {
|
||||
var _this2 = this;
|
||||
|
||||
var intervalMs = options.intervalMs,
|
||||
consolidateBy = options.consolidateBy;
|
||||
|
||||
var intervalSec = Math.ceil(intervalMs / 1000);
|
||||
|
||||
consolidateBy = consolidateBy || 'avg';
|
||||
var aggFunction = consolidateByFunc[consolidateBy];
|
||||
|
||||
// Group items by value type and perform request for each value type
|
||||
var grouped_items = _lodash2.default.groupBy(items, 'value_type');
|
||||
var promises = _lodash2.default.map(grouped_items, function (items, value_type) {
|
||||
var itemids = _lodash2.default.map(items, 'itemid').join(', ');
|
||||
var table = TREND_TO_TABLE_MAP[value_type];
|
||||
var valueColumn = _lodash2.default.includes(['avg', 'min', 'max'], consolidateBy) ? consolidateBy : 'avg';
|
||||
valueColumn = consolidateByTrendColumns[valueColumn];
|
||||
|
||||
var query = '\n SELECT itemid AS metric, clock AS time_sec, ' + aggFunction + '(' + valueColumn + ') as value\n FROM ' + table + '\n WHERE itemid IN (' + itemids + ')\n AND clock > ' + timeFrom + ' AND clock < ' + timeTill + '\n GROUP BY time_sec DIV ' + intervalSec + ', metric\n ';
|
||||
|
||||
query = compactSQLQuery(query);
|
||||
return _this2.invokeSQLQuery(query);
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(function (results) {
|
||||
return _lodash2.default.flatten(results);
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: 'handleGrafanaTSResponse',
|
||||
value: function handleGrafanaTSResponse(history, items) {
|
||||
var addHostName = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
|
||||
|
||||
return convertGrafanaTSResponse(history, items, addHostName);
|
||||
}
|
||||
}, {
|
||||
key: 'invokeSQLQuery',
|
||||
value: function invokeSQLQuery(query) {
|
||||
var queryDef = {
|
||||
refId: 'A',
|
||||
format: 'time_series',
|
||||
datasourceId: this.sqlDataSourceId,
|
||||
rawSql: query,
|
||||
maxDataPoints: this.limit
|
||||
};
|
||||
|
||||
return backendSrv.datasourceRequest({
|
||||
url: '/api/tsdb/query',
|
||||
method: 'POST',
|
||||
data: {
|
||||
queries: [queryDef]
|
||||
}
|
||||
}).then(function (response) {
|
||||
var results = response.data.results;
|
||||
if (results['A']) {
|
||||
return results['A'].series;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}]);
|
||||
|
||||
return ZabbixDBConnector;
|
||||
}();
|
||||
|
||||
return ZabbixDBConnector;
|
||||
}
|
||||
|
||||
_angular2.default.module('grafana.services').factory('ZabbixDBConnector', ZabbixDBConnectorFactory);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function convertGrafanaTSResponse(time_series, items, addHostName) {
|
||||
var hosts = _lodash2.default.uniqBy(_lodash2.default.flatten(_lodash2.default.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate
|
||||
var grafanaSeries = _lodash2.default.map(time_series, function (series) {
|
||||
var itemid = series.name;
|
||||
var datapoints = series.points;
|
||||
var item = _lodash2.default.find(items, { 'itemid': itemid });
|
||||
var alias = item.name;
|
||||
if (_lodash2.default.keys(hosts).length > 1 && addHostName) {
|
||||
//only when actual multi hosts selected
|
||||
var host = _lodash2.default.find(hosts, { 'hostid': item.hostid });
|
||||
alias = host.name + ": " + alias;
|
||||
}
|
||||
return {
|
||||
target: alias,
|
||||
datapoints: datapoints
|
||||
};
|
||||
});
|
||||
|
||||
return _lodash2.default.sortBy(grafanaSeries, 'target');
|
||||
}
|
||||
|
||||
function compactSQLQuery(query) {
|
||||
return query.replace(/\s+/g, ' ');
|
||||
}
|
||||
@@ -18,6 +18,7 @@ pages:
|
||||
- Installation:
|
||||
- 'Installation': 'installation/index.md'
|
||||
- 'Configuration': 'installation/configuration.md'
|
||||
- 'SQL Data Source Configuration': 'installation/configuration-sql.md'
|
||||
- 'Upgrade': 'installation/upgrade.md'
|
||||
- 'Troubleshooting': 'installation/troubleshooting.md'
|
||||
- User Guides:
|
||||
@@ -28,5 +29,6 @@ pages:
|
||||
- 'Triggers Panel': 'reference/panel-triggers.md'
|
||||
- 'Functions': 'reference/functions.md'
|
||||
- 'Alerting': 'reference/alerting.md'
|
||||
- 'Direct DB Connection': 'reference/direct-db-connection.md'
|
||||
# - Tutorials:
|
||||
# - 'Building Host Dashboard': 'tutorials/host_dashboard.md'
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6b58742637f58519a5fab038db0c15b5525d1d330fe5e4a1de954b72f4f1cc3e
|
||||
size 225446
|
||||
oid sha256:367b5fa51e9d05eb87afedd4ef6ed8ea1ce0e095f94bfdc58ebe6744fbaca71c
|
||||
size 131343
|
||||
|
||||
3
docs/sources/img/installation-mysql_ds_config.png
Normal file
3
docs/sources/img/installation-mysql_ds_config.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0bac25f3d28c74f1f309476120bc67275e15f420e3343f0ff9668ead3a1e6170
|
||||
size 227988
|
||||
22
docs/sources/installation/configuration-sql.md
Normal file
22
docs/sources/installation/configuration-sql.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# SQL Data Source Configuration
|
||||
|
||||
In order to use _Direct DB Connection_ feature you should configure SQL data source first.
|
||||
|
||||

|
||||
|
||||
Select _MySQL_ data source type and provide your database host address and port (3306 is default for MySQL). Fill
|
||||
database name (usually, `zabbix`) and specify credentials.
|
||||
|
||||
## Security notes
|
||||
|
||||
As you can see in _User Permission_ note, Grafana doesn't restrict any queries to the database. So you should be careful
|
||||
and create a special user with limited access to Zabbix database. Grafana-Zabbix plugin uses only `SELECT` queries to
|
||||
`history`, `history_uint`, `trends` and `trends_uint` tables. So it's reasonable to grant only SELECT privileges to
|
||||
these tables for grafana user. But if you want to use this MySQL data source for querying another data, you can
|
||||
grant SELECT privileges to entire zabbix database.
|
||||
|
||||
Also, all queries are invoked by grafana-server, so you can restrict connection to only grafana host.
|
||||
|
||||
```sql
|
||||
GRANT SELECT ON zabbix.* TO 'grafana'@'grafana-host' identified by 'password';
|
||||
```
|
||||
@@ -52,7 +52,23 @@ Direct access is still supported because in some cases it may be useful to acces
|
||||
- **Cache TTL**: plugin caches some api requests for increasing performance. Set this
|
||||
value to desired cache lifetime (this option affect data like items list).
|
||||
|
||||
### Direct DB Connection
|
||||
|
||||
Direct DB Connection allows plugin to use existing SQL data source for querying history data directly from Zabbix
|
||||
database. This way usually faster than pulling data from Zabbix API, especially on the wide time ranges, and reduces
|
||||
amount of data transfered.
|
||||
|
||||
Read [how to configure](/installation/configuration-sql) SQL data source in Grafana.
|
||||
|
||||
- **Enable**: enable Direct DB Connection.
|
||||
- **SQL Data Source**: Select SQL Data Source for Zabbix database.
|
||||
|
||||
#### Supported databases
|
||||
|
||||
Now only **MySQL** is supported by Grafana.
|
||||
|
||||
### Alerting
|
||||
|
||||
- **Enable alerting**: enable limited alerting support.
|
||||
- **Add thresholds**: get thresholds info from zabbix triggers and add it to graphs.
|
||||
For example, if you have trigger `{Zabbix server:system.cpu.util[,iowait].avg(5m)}>20`, threshold will be set to 20.
|
||||
@@ -65,10 +81,12 @@ or password, wrong api url.
|
||||

|
||||
|
||||
## Import example dashboards
|
||||
|
||||
You can import dashboard examples from _Dashboards_ tab in plugin config.
|
||||

|
||||
|
||||
## Note about Zabbix 2.2 or less
|
||||
|
||||
Zabbix API (api_jsonrpc.php) before zabbix 2.4 don't allow cross-domain requests (CORS). And you
|
||||
can get HTTP error 412 (Precondition Failed).
|
||||
To fix it add this code to api_jsonrpc.php immediately after the copyright:
|
||||
@@ -83,17 +101,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
before
|
||||
|
||||
```php
|
||||
require_once dirname(__FILE__).'/include/func.inc.php';
|
||||
require_once dirname(__FILE__).'/include/classes/core/CHttpRequest.php';
|
||||
```
|
||||
|
||||
[Full fix listing](https://gist.github.com/alexanderzobnin/f2348f318d7a93466a0c).
|
||||
For more details see zabbix issues [ZBXNEXT-1377](https://support.zabbix.com/browse/ZBXNEXT-1377)
|
||||
and [ZBX-8459](https://support.zabbix.com/browse/ZBX-8459).
|
||||
|
||||
## Note about Browser Cache
|
||||
|
||||
After updating plugin, clear browser cache and reload application page. See details
|
||||
for [Chrome](https://support.google.com/chrome/answer/95582),
|
||||
[Firefox](https://support.mozilla.org/en-US/kb/how-clear-firefox-cache). You need to clear cache
|
||||
|
||||
43
docs/sources/reference/direct-db-connection.md
Normal file
43
docs/sources/reference/direct-db-connection.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Direct DB Connection
|
||||
|
||||
Since version 4.3 Grafana has MySQL data source, Grafana-Zabbix plugin can use it for querying data directly from
|
||||
Zabbix database.
|
||||
|
||||
One of the most hard queries for Zabbix API is history queries. For long time intervals `history.get`
|
||||
returns huge amount of data. In order to display it, plugin should adjust time series resolution
|
||||
by using [consolidateBy](/reference/functions/#consolidateby) function. Ultimately, Grafana displays this reduced
|
||||
time series, but that data should be loaded and processed on the client side first. Direct DB Connection solves this
|
||||
two problems by moving consolidation to the server side. Thus, client get ready-to-use dataset which has much smaller
|
||||
size. Data loads faster and client don't spend time for data processing.
|
||||
|
||||
Also, many users point better performance of direct database queries versus API calls. This caused by several reasons,
|
||||
such as additional PHP layer and additional SQL queries (user permissions checks).
|
||||
|
||||
## Query structure
|
||||
|
||||
Grafana-Zabbix uses queries like this for getting history:
|
||||
|
||||
```sql
|
||||
SELECT itemid AS metric, clock AS time_sec, {aggFunc}(value) as value
|
||||
FROM {historyTable}
|
||||
WHERE itemid IN ({itemids})
|
||||
AND clock > {timeFrom} AND clock < {timeTill}
|
||||
GROUP BY time_sec DIV {intervalSec}, metric
|
||||
```
|
||||
|
||||
where `{aggFunc}` is one of `[AVG, MIN, MAX, SUM, COUNT]` aggregation function, `{historyTable}` is a history table,
|
||||
`{intervalSec}` - consolidation interval in seconds.
|
||||
|
||||
When getting trends, plugin additionally queries particular value column (`value_avg`, `value_min` or `value_max`)
|
||||
depends on `consolidateBy` function value:
|
||||
|
||||
```sql
|
||||
SELECT itemid AS metric, clock AS time_sec, {aggFunc}({valueColumn}) as value
|
||||
FROM {trendsTable}
|
||||
WHERE itemid IN ({itemids})
|
||||
AND clock > {timeFrom} AND clock < {timeTill}
|
||||
GROUP BY time_sec DIV {intervalSec}, metric
|
||||
```
|
||||
|
||||
As you can see, plugin uses aggregation by given time interval. This interval is provided by Grafana and depends on the
|
||||
panel with in pixels. Thus, Grafana always gets data in necessary resolution.
|
||||
@@ -197,3 +197,14 @@ replaceAlias(/.*CPU (.*) time/, $1) -> system
|
||||
backend01: CPU system time
|
||||
replaceAlias(/(.*): CPU (.*) time/, $1 - $2) -> backend01 - system
|
||||
```
|
||||
|
||||
## Special
|
||||
|
||||
### consolidateBy
|
||||
```
|
||||
consolidateBy(consolidationFunc)
|
||||
```
|
||||
|
||||
When a graph is drawn where width of the graph size in pixels is smaller than the number of datapoints to be graphed, plugin consolidates the values to to prevent line overlap. The consolidateBy() function changes the consolidation function from the default of average to one of `sum`, `min`, `max` or `count`.
|
||||
|
||||
Valid function names are `sum`, `avg`, `min`, `max` and `count`.
|
||||
|
||||
28
src/datasource-zabbix/config.controller.js
Normal file
28
src/datasource-zabbix/config.controller.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
const SUPPORTED_SQL_DS = ['mysql'];
|
||||
|
||||
const defaultConfig = {
|
||||
dbConnection: {
|
||||
enable: false,
|
||||
}
|
||||
};
|
||||
|
||||
export class ZabbixDSConfigController {
|
||||
/** @ngInject */
|
||||
constructor($scope, $injector, datasourceSrv) {
|
||||
this.datasourceSrv = datasourceSrv;
|
||||
|
||||
_.defaults(this.current.jsonData, defaultConfig);
|
||||
this.sqlDataSources = this.getSupportedSQLDataSources();
|
||||
}
|
||||
|
||||
getSupportedSQLDataSources() {
|
||||
let datasources = this.datasourceSrv.getAll();
|
||||
return _.filter(datasources, ds => {
|
||||
return _.includes(SUPPORTED_SQL_DS, ds.type);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ZabbixDSConfigController.templateUrl = 'datasource-zabbix/partials/config.html';
|
||||
@@ -19,6 +19,9 @@ class ZabbixAPIDatasource {
|
||||
this.dashboardSrv = dashboardSrv;
|
||||
this.zabbixAlertingSrv = zabbixAlertingSrv;
|
||||
|
||||
// Use custom format for template variables
|
||||
this.replaceTemplateVars = _.partial(replaceTemplateVars, this.templateSrv);
|
||||
|
||||
// General data source settings
|
||||
this.name = instanceSettings.name;
|
||||
this.url = instanceSettings.url;
|
||||
@@ -43,10 +46,21 @@ class ZabbixAPIDatasource {
|
||||
this.addThresholds = instanceSettings.jsonData.addThresholds;
|
||||
this.alertingMinSeverity = instanceSettings.jsonData.alertingMinSeverity || c.SEV_WARNING;
|
||||
|
||||
this.zabbix = new Zabbix(this.url, this.username, this.password, this.basicAuth, this.withCredentials, this.cacheTTL);
|
||||
// Direct DB Connection options
|
||||
this.enableDirectDBConnection = instanceSettings.jsonData.dbConnection.enable;
|
||||
this.sqlDatasourceId = instanceSettings.jsonData.dbConnection.datasourceId;
|
||||
|
||||
// Use custom format for template variables
|
||||
this.replaceTemplateVars = _.partial(replaceTemplateVars, this.templateSrv);
|
||||
let zabbixOptions = {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
basicAuth: this.basicAuth,
|
||||
withCredentials: this.withCredentials,
|
||||
cacheTTL: this.cacheTTL,
|
||||
enableDirectDBConnection: this.enableDirectDBConnection,
|
||||
sqlDatasourceId: this.sqlDatasourceId
|
||||
};
|
||||
|
||||
this.zabbix = new Zabbix(this.url, zabbixOptions);
|
||||
}
|
||||
|
||||
////////////////////////
|
||||
@@ -140,27 +154,33 @@ class ZabbixAPIDatasource {
|
||||
return this.zabbix.getItemsFromTarget(target, getItemOptions)
|
||||
.then(items => {
|
||||
let getHistoryPromise;
|
||||
options.consolidateBy = getConsolidateBy(target);
|
||||
|
||||
if (useTrends) {
|
||||
if (this.enableDirectDBConnection) {
|
||||
getHistoryPromise = this.zabbix.getTrendsDB(items, timeFrom, timeTo, options)
|
||||
.then(history => this.zabbix.dbConnector.handleGrafanaTSResponse(history, items));
|
||||
} else {
|
||||
let valueType = this.getTrendValueType(target);
|
||||
getHistoryPromise = this.zabbix.getTrend(items, timeFrom, timeTo)
|
||||
.then(history => {
|
||||
return responseHandler.handleTrends(history, items, valueType);
|
||||
})
|
||||
.then(history => responseHandler.handleTrends(history, items, valueType))
|
||||
.then(timeseries => {
|
||||
// Sort trend data, issue #202
|
||||
_.forEach(timeseries, series => {
|
||||
series.datapoints = _.sortBy(series.datapoints, point => point[c.DATAPOINT_TS]);
|
||||
});
|
||||
|
||||
return timeseries;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Use history
|
||||
if (this.enableDirectDBConnection) {
|
||||
getHistoryPromise = this.zabbix.getHistoryDB(items, timeFrom, timeTo, options)
|
||||
.then(history => this.zabbix.dbConnector.handleGrafanaTSResponse(history, items));
|
||||
} else {
|
||||
getHistoryPromise = this.zabbix.getHistory(items, timeFrom, timeTo)
|
||||
.then(history => {
|
||||
return responseHandler.handleHistory(history, items);
|
||||
});
|
||||
.then(history => responseHandler.handleHistory(history, items));
|
||||
}
|
||||
}
|
||||
|
||||
return getHistoryPromise;
|
||||
@@ -511,11 +531,24 @@ function bindFunctionDefs(functionDefs, category) {
|
||||
});
|
||||
}
|
||||
|
||||
function getConsolidateBy(target) {
|
||||
let consolidateBy = 'avg';
|
||||
let funcDef = _.find(target.functions, func => {
|
||||
return func.def.name === 'consolidateBy';
|
||||
});
|
||||
if (funcDef && funcDef.params && funcDef.params.length) {
|
||||
consolidateBy = funcDef.params[0];
|
||||
}
|
||||
return consolidateBy;
|
||||
}
|
||||
|
||||
function downsampleSeries(timeseries_data, options) {
|
||||
let defaultAgg = dataProcessor.aggregationFunctions['avg'];
|
||||
let consolidateByFunc = dataProcessor.aggregationFunctions[options.consolidateBy] || defaultAgg;
|
||||
return _.map(timeseries_data, timeseries => {
|
||||
if (timeseries.datapoints.length > options.maxDataPoints) {
|
||||
timeseries.datapoints = dataProcessor
|
||||
.groupBy(options.interval, dataProcessor.AVERAGE, timeseries.datapoints);
|
||||
.groupBy(options.interval, consolidateByFunc, timeseries.datapoints);
|
||||
}
|
||||
return timeseries;
|
||||
});
|
||||
|
||||
@@ -8,7 +8,8 @@ var categories = {
|
||||
Filter: [],
|
||||
Trends: [],
|
||||
Time: [],
|
||||
Alias: []
|
||||
Alias: [],
|
||||
Special: []
|
||||
};
|
||||
|
||||
function addFuncDef(funcDef) {
|
||||
@@ -204,6 +205,16 @@ addFuncDef({
|
||||
defaultParams: ['/(.*)/', '$1']
|
||||
});
|
||||
|
||||
// Special
|
||||
addFuncDef({
|
||||
name: 'consolidateBy',
|
||||
category: 'Special',
|
||||
params: [
|
||||
{ name: 'type', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count'] }
|
||||
],
|
||||
defaultParams: ['avg'],
|
||||
});
|
||||
|
||||
_.each(categories, function(funcList, catName) {
|
||||
categories[catName] = _.sortBy(funcList, 'name');
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {ZabbixAPIDatasource} from './datasource';
|
||||
import {ZabbixQueryController} from './query.controller';
|
||||
|
||||
class ZabbixConfigController {}
|
||||
ZabbixConfigController.templateUrl = 'datasource-zabbix/partials/config.html';
|
||||
import {ZabbixDSConfigController} from './config.controller';
|
||||
|
||||
class ZabbixQueryOptionsController {}
|
||||
ZabbixQueryOptionsController.templateUrl = 'datasource-zabbix/partials/query.options.html';
|
||||
@@ -12,7 +10,7 @@ ZabbixAnnotationsQueryController.templateUrl = 'datasource-zabbix/partials/annot
|
||||
|
||||
export {
|
||||
ZabbixAPIDatasource as Datasource,
|
||||
ZabbixConfigController as ConfigCtrl,
|
||||
ZabbixDSConfigController as ConfigCtrl,
|
||||
ZabbixQueryController as QueryCtrl,
|
||||
ZabbixQueryOptionsController as QueryOptionsCtrl,
|
||||
ZabbixAnnotationsQueryController as AnnotationsQueryCtrl
|
||||
|
||||
@@ -75,18 +75,46 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h3 class="page-heading">Direct DB Connection</h3>
|
||||
<gf-form-switch class="gf-form" label-class="width-12"
|
||||
label="Enable"
|
||||
checked="ctrl.current.jsonData.dbConnection.enable">
|
||||
</gf-form-switch>
|
||||
<div ng-if="ctrl.current.jsonData.dbConnection.enable">
|
||||
<div class="gf-form max-width-20">
|
||||
<span class="gf-form-label width-12">
|
||||
SQL Data Source
|
||||
<info-popover mode="right-normal">
|
||||
Select SQL Data Source for Zabbix database.
|
||||
In order to use this feature you should <a href="/datasources/new" target="_blank">create</a> and
|
||||
configure it first. Zabbix plugin uses this data source for querying history data directly from database.
|
||||
This way usually faster than pulling data from Zabbix API, especially on the wide time ranges, and reduces
|
||||
amount of data transfered.
|
||||
</info-popover>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper max-width-16">
|
||||
<select class="gf-form-input" ng-model="ctrl.current.jsonData.dbConnection.datasourceId"
|
||||
ng-options="ds.id as ds.name for ds in ctrl.sqlDataSources">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h3 class="page-heading">Alerting</h3>
|
||||
<gf-form-switch class="gf-form" label-class="width-9"
|
||||
<gf-form-switch class="gf-form" label-class="width-12"
|
||||
label="Enable alerting"
|
||||
checked="ctrl.current.jsonData.alerting">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label-class="width-9"
|
||||
<div ng-if="ctrl.current.jsonData.alerting">
|
||||
<gf-form-switch class="gf-form" label-class="width-12"
|
||||
label="Add thresholds"
|
||||
checked="ctrl.current.jsonData.addThresholds">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form max-width-20">
|
||||
<span class="gf-form-label width-9">Min severity</span>
|
||||
<span class="gf-form-label width-12">Min severity</span>
|
||||
<div class="gf-form-select-wrapper max-width-16">
|
||||
<select class="gf-form-input" ng-model="ctrl.current.jsonData.alertingMinSeverity"
|
||||
ng-options="s.val as s.text for s in [
|
||||
@@ -96,4 +124,5 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,10 @@ describe('ZabbixDatasource', () => {
|
||||
password: 'zabbix',
|
||||
trends: true,
|
||||
trendsFrom: '14d',
|
||||
trendsRange: '7d'
|
||||
trendsRange: '7d',
|
||||
dbConnection: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
};
|
||||
ctx.templateSrv = {};
|
||||
|
||||
@@ -3,31 +3,45 @@ import _ from 'lodash';
|
||||
import * as utils from './utils';
|
||||
import './zabbixAPI.service.js';
|
||||
import './zabbixCachingProxy.service.js';
|
||||
import './zabbixDBConnector';
|
||||
|
||||
// Use factory() instead service() for multiple data sources support.
|
||||
// Each Zabbix data source instance should initialize its own API instance.
|
||||
|
||||
/** @ngInject */
|
||||
function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy) {
|
||||
function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy, ZabbixDBConnector) {
|
||||
|
||||
class Zabbix {
|
||||
constructor(url, username, password, basicAuth, withCredentials, cacheTTL) {
|
||||
constructor(url, options) {
|
||||
let {
|
||||
username, password, basicAuth, withCredentials, cacheTTL,
|
||||
enableDirectDBConnection, sqlDatasourceId
|
||||
} = options;
|
||||
|
||||
// Initialize Zabbix API
|
||||
var ZabbixAPI = zabbixAPIService;
|
||||
this.zabbixAPI = new ZabbixAPI(url, username, password, basicAuth, withCredentials);
|
||||
|
||||
if (enableDirectDBConnection) {
|
||||
this.dbConnector = new ZabbixDBConnector(sqlDatasourceId);
|
||||
}
|
||||
|
||||
// Initialize caching proxy for requests
|
||||
let cacheOptions = {
|
||||
enabled: true,
|
||||
ttl: cacheTTL
|
||||
};
|
||||
this.cachingProxy = new ZabbixCachingProxy(this.zabbixAPI, cacheOptions);
|
||||
this.cachingProxy = new ZabbixCachingProxy(this.zabbixAPI, this.dbConnector, cacheOptions);
|
||||
|
||||
// Proxy methods
|
||||
this.getHistory = this.cachingProxy.getHistory.bind(this.cachingProxy);
|
||||
this.getMacros = this.cachingProxy.getMacros.bind(this.cachingProxy);
|
||||
|
||||
if (enableDirectDBConnection) {
|
||||
this.getHistoryDB = this.cachingProxy.getHistoryDB.bind(this.cachingProxy);
|
||||
this.getTrendsDB = this.cachingProxy.getTrendsDB.bind(this.cachingProxy);
|
||||
}
|
||||
|
||||
this.getTrend = this.zabbixAPI.getTrend.bind(this.zabbixAPI);
|
||||
this.getEvents = this.zabbixAPI.getEvents.bind(this.zabbixAPI);
|
||||
this.getAlerts = this.zabbixAPI.getAlerts.bind(this.zabbixAPI);
|
||||
|
||||
@@ -8,8 +8,9 @@ import _ from 'lodash';
|
||||
function ZabbixCachingProxyFactory() {
|
||||
|
||||
class ZabbixCachingProxy {
|
||||
constructor(zabbixAPI, cacheOptions) {
|
||||
constructor(zabbixAPI, zabbixDBConnector, cacheOptions) {
|
||||
this.zabbixAPI = zabbixAPI;
|
||||
this.dbConnector = zabbixDBConnector;
|
||||
this.cacheEnabled = cacheOptions.enabled;
|
||||
this.ttl = cacheOptions.ttl || 600000; // 10 minutes by default
|
||||
|
||||
@@ -31,6 +32,13 @@ function ZabbixCachingProxyFactory() {
|
||||
this.getHistory = callAPIRequestOnce(_.bind(this.zabbixAPI.getHistory, this.zabbixAPI),
|
||||
this.historyPromises, getHistoryRequestHash);
|
||||
|
||||
if (this.dbConnector) {
|
||||
this.getHistoryDB = callAPIRequestOnce(_.bind(this.dbConnector.getHistory, this.dbConnector),
|
||||
this.historyPromises, getDBQueryHash);
|
||||
this.getTrendsDB = callAPIRequestOnce(_.bind(this.dbConnector.getTrends, this.dbConnector),
|
||||
this.historyPromises, getDBQueryHash);
|
||||
}
|
||||
|
||||
// Don't run duplicated requests
|
||||
this.groupPromises = {};
|
||||
this.getGroupsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getGroups, this.zabbixAPI),
|
||||
@@ -195,6 +203,14 @@ function getHistoryRequestHash(args) {
|
||||
return stamp.getHash();
|
||||
}
|
||||
|
||||
function getDBQueryHash(args) {
|
||||
let itemids = _.map(args[0], 'itemid');
|
||||
let consolidateBy = args[3].consolidateBy;
|
||||
let intervalMs = args[3].intervalMs;
|
||||
let stamp = itemids.join() + args[1] + args[2] + consolidateBy + intervalMs;
|
||||
return stamp.getHash();
|
||||
}
|
||||
|
||||
String.prototype.getHash = function() {
|
||||
var hash = 0, i, chr, len;
|
||||
if (this.length !== 0) {
|
||||
|
||||
183
src/datasource-zabbix/zabbixDBConnector.js
Normal file
183
src/datasource-zabbix/zabbixDBConnector.js
Normal file
@@ -0,0 +1,183 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
|
||||
const DEFAULT_QUERY_LIMIT = 10000;
|
||||
const HISTORY_TO_TABLE_MAP = {
|
||||
'0': 'history',
|
||||
'1': 'history_str',
|
||||
'2': 'history_log',
|
||||
'3': 'history_uint',
|
||||
'4': 'history_text'
|
||||
};
|
||||
|
||||
const TREND_TO_TABLE_MAP = {
|
||||
'0': 'trends',
|
||||
'3': 'trends_uint'
|
||||
};
|
||||
|
||||
const consolidateByFunc = {
|
||||
'avg': 'AVG',
|
||||
'min': 'MIN',
|
||||
'max': 'MAX',
|
||||
'sum': 'SUM',
|
||||
'count': 'COUNT'
|
||||
};
|
||||
|
||||
const consolidateByTrendColumns = {
|
||||
'avg': 'value_avg',
|
||||
'min': 'value_min',
|
||||
'max': 'value_max'
|
||||
};
|
||||
|
||||
/** @ngInject */
|
||||
function ZabbixDBConnectorFactory(datasourceSrv, backendSrv) {
|
||||
|
||||
class ZabbixDBConnector {
|
||||
|
||||
constructor(sqlDataSourceId, options = {}) {
|
||||
let {limit} = options;
|
||||
|
||||
this.sqlDataSourceId = sqlDataSourceId;
|
||||
this.limit = limit || DEFAULT_QUERY_LIMIT;
|
||||
|
||||
// Try to load DS with given id to check it's exist
|
||||
this.loadSQLDataSource(sqlDataSourceId);
|
||||
}
|
||||
|
||||
loadSQLDataSource(datasourceId) {
|
||||
let ds = _.find(datasourceSrv.getAll(), {'id': datasourceId});
|
||||
if (ds) {
|
||||
return datasourceSrv.loadDatasource(ds.name)
|
||||
.then(ds => {
|
||||
console.log('SQL data source loaded', ds);
|
||||
});
|
||||
} else {
|
||||
return Promise.reject(`SQL Data Source with ID ${datasourceId} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
getHistory(items, timeFrom, timeTill, options) {
|
||||
let {intervalMs, consolidateBy} = options;
|
||||
let intervalSec = Math.ceil(intervalMs / 1000);
|
||||
|
||||
consolidateBy = consolidateBy || 'avg';
|
||||
let aggFunction = consolidateByFunc[consolidateBy];
|
||||
|
||||
// Group items by value type and perform request for each value type
|
||||
let grouped_items = _.groupBy(items, 'value_type');
|
||||
let promises = _.map(grouped_items, (items, value_type) => {
|
||||
let itemids = _.map(items, 'itemid').join(', ');
|
||||
let table = HISTORY_TO_TABLE_MAP[value_type];
|
||||
|
||||
let query = `
|
||||
SELECT itemid AS metric, clock AS time_sec, ${aggFunction}(value) as value
|
||||
FROM ${table}
|
||||
WHERE itemid IN (${itemids})
|
||||
AND clock > ${timeFrom} AND clock < ${timeTill}
|
||||
GROUP BY time_sec DIV ${intervalSec}, metric
|
||||
`;
|
||||
|
||||
query = compactSQLQuery(query);
|
||||
return this.invokeSQLQuery(query);
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(results => {
|
||||
return _.flatten(results);
|
||||
});
|
||||
}
|
||||
|
||||
getTrends(items, timeFrom, timeTill, options) {
|
||||
let {intervalMs, consolidateBy} = options;
|
||||
let intervalSec = Math.ceil(intervalMs / 1000);
|
||||
|
||||
consolidateBy = consolidateBy || 'avg';
|
||||
let aggFunction = consolidateByFunc[consolidateBy];
|
||||
|
||||
// Group items by value type and perform request for each value type
|
||||
let grouped_items = _.groupBy(items, 'value_type');
|
||||
let promises = _.map(grouped_items, (items, value_type) => {
|
||||
let itemids = _.map(items, 'itemid').join(', ');
|
||||
let table = TREND_TO_TABLE_MAP[value_type];
|
||||
let valueColumn = _.includes(['avg', 'min', 'max'], consolidateBy) ? consolidateBy : 'avg';
|
||||
valueColumn = consolidateByTrendColumns[valueColumn];
|
||||
|
||||
let query = `
|
||||
SELECT itemid AS metric, clock AS time_sec, ${aggFunction}(${valueColumn}) as value
|
||||
FROM ${table}
|
||||
WHERE itemid IN (${itemids})
|
||||
AND clock > ${timeFrom} AND clock < ${timeTill}
|
||||
GROUP BY time_sec DIV ${intervalSec}, metric
|
||||
`;
|
||||
|
||||
query = compactSQLQuery(query);
|
||||
return this.invokeSQLQuery(query);
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(results => {
|
||||
return _.flatten(results);
|
||||
});
|
||||
}
|
||||
|
||||
handleGrafanaTSResponse(history, items, addHostName = true) {
|
||||
return convertGrafanaTSResponse(history, items, addHostName);
|
||||
}
|
||||
|
||||
invokeSQLQuery(query) {
|
||||
let queryDef = {
|
||||
refId: 'A',
|
||||
format: 'time_series',
|
||||
datasourceId: this.sqlDataSourceId,
|
||||
rawSql: query,
|
||||
maxDataPoints: this.limit
|
||||
};
|
||||
|
||||
return backendSrv.datasourceRequest({
|
||||
url: '/api/tsdb/query',
|
||||
method: 'POST',
|
||||
data: {
|
||||
queries: [queryDef],
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
let results = response.data.results;
|
||||
if (results['A']) {
|
||||
return results['A'].series;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return ZabbixDBConnector;
|
||||
}
|
||||
|
||||
angular
|
||||
.module('grafana.services')
|
||||
.factory('ZabbixDBConnector', ZabbixDBConnectorFactory);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function convertGrafanaTSResponse(time_series, items, addHostName) {
|
||||
var hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate
|
||||
let grafanaSeries = _.map(time_series, series => {
|
||||
let itemid = series.name;
|
||||
let datapoints = series.points;
|
||||
var item = _.find(items, {'itemid': itemid});
|
||||
var alias = item.name;
|
||||
if (_.keys(hosts).length > 1 && addHostName) { //only when actual multi hosts selected
|
||||
var host = _.find(hosts, {'hostid': item.hostid});
|
||||
alias = host.name + ": " + alias;
|
||||
}
|
||||
return {
|
||||
target: alias,
|
||||
datapoints: datapoints
|
||||
};
|
||||
});
|
||||
|
||||
return _.sortBy(grafanaSeries, 'target');
|
||||
}
|
||||
|
||||
function compactSQLQuery(query) {
|
||||
return query.replace(/\s+/g, ' ');
|
||||
}
|
||||
Reference in New Issue
Block a user